// Copyright 2014 Google Inc. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd #include #include "packager/base/files/file_util.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/stringprintf.h" #include "packager/base/time/clock.h" #include "packager/media/base/demuxer.h" #include "packager/media/base/fixed_key_source.h" #include "packager/media/base/fourccs.h" #include "packager/media/base/media_stream.h" #include "packager/media/base/muxer.h" #include "packager/media/base/stream_info.h" #include "packager/media/base/test/status_test_util.h" #include "packager/media/formats/mp4/mp4_muxer.h" #include "packager/media/test/test_data_util.h" using ::testing::ValuesIn; namespace shaka { namespace media { namespace { const char* kMediaFiles[] = {"bear-640x360.mp4", "bear-640x360-av_frag.mp4", "bear-640x360.ts"}; // Muxer options. const double kSegmentDurationInSeconds = 1.0; const double kFragmentDurationInSecodns = 0.1; const bool kSegmentSapAligned = true; const bool kFragmentSapAligned = true; const int kNumSubsegmentsPerSidx = 2; const char kOutputVideo[] = "output_video"; const char kOutputVideo2[] = "output_video_2"; const char kOutputAudio[] = "output_audio"; const char kOutputAudio2[] = "output_audio_2"; const char kOutputNone[] = ""; const char kSegmentTemplate[] = "template$Number$.m4s"; const char kSegmentTemplateOutputPattern[] = "template%d.m4s"; const bool kSingleSegment = true; const bool kMultipleSegments = false; const bool kEnableEncryption = true; const bool kDisableEncryption = false; const char kNoLanguageOverride[] = ""; // Encryption constants. const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; const char kKeyHex[] = "6fc96fe628a265b13aeddec0bc421f4d"; const double kClearLeadInSeconds = 1.5; const double kCryptoDurationInSeconds = 0; // Key rotation is disabled. MediaStream* FindFirstStreamOfType(const std::vector& streams, StreamType stream_type) { typedef std::vector::const_iterator StreamIterator; for (StreamIterator it = streams.begin(); it != streams.end(); ++it) { if ((*it)->info()->stream_type() == stream_type) return *it; } return NULL; } MediaStream* FindFirstVideoStream(const std::vector& streams) { return FindFirstStreamOfType(streams, kStreamVideo); } MediaStream* FindFirstAudioStream(const std::vector& streams) { return FindFirstStreamOfType(streams, kStreamAudio); } } // namespace class FakeClock : public base::Clock { public: // Fake the clock to return NULL time. base::Time Now() override { return base::Time(); } }; class PackagerTestBasic : public ::testing::TestWithParam { public: PackagerTestBasic() {} void SetUp() override { // Create a test directory for testing, will be deleted after test. ASSERT_TRUE(base::CreateNewTempDirectory("packager_", &test_directory_)); // Copy the input to test directory for easy reference. ASSERT_TRUE(base::CopyFile(GetTestDataFilePath(GetParam()), test_directory_.AppendASCII(GetParam()))); } void TearDown() override { base::DeleteFile(test_directory_, true); } std::string GetFullPath(const std::string& file_name); // Check if |file1| and |file2| are the same. bool ContentsEqual(const std::string& file1, const std::string file2); MuxerOptions SetupOptions(const std::string& output, bool single_segment); void Remux(const std::string& input, const std::string& video_output, const std::string& audio_output, bool single_segment, bool enable_encryption, const std::string& override_language); void Decrypt(const std::string& input, const std::string& video_output, const std::string& audio_output); protected: base::FilePath test_directory_; FakeClock fake_clock_; }; std::string PackagerTestBasic::GetFullPath(const std::string& file_name) { return test_directory_.AppendASCII(file_name).value(); } bool PackagerTestBasic::ContentsEqual(const std::string& file1, const std::string file2) { return base::ContentsEqual(test_directory_.AppendASCII(file1), test_directory_.AppendASCII(file2)); } MuxerOptions PackagerTestBasic::SetupOptions(const std::string& output, bool single_segment) { MuxerOptions options; options.single_segment = single_segment; options.segment_duration = kSegmentDurationInSeconds; options.fragment_duration = kFragmentDurationInSecodns; options.segment_sap_aligned = kSegmentSapAligned; options.fragment_sap_aligned = kFragmentSapAligned; options.num_subsegments_per_sidx = kNumSubsegmentsPerSidx; options.output_file_name = GetFullPath(output); options.segment_template = GetFullPath(kSegmentTemplate); options.temp_dir = test_directory_.value(); return options; } void PackagerTestBasic::Remux(const std::string& input, const std::string& video_output, const std::string& audio_output, bool single_segment, bool enable_encryption, const std::string& language_override) { CHECK(!video_output.empty() || !audio_output.empty()); Demuxer demuxer(GetFullPath(input)); ASSERT_OK(demuxer.Initialize()); scoped_ptr encryption_key_source( FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); DCHECK(encryption_key_source); scoped_ptr muxer_video; if (!video_output.empty()) { muxer_video.reset( new mp4::MP4Muxer(SetupOptions(video_output, single_segment))); muxer_video->set_clock(&fake_clock_); MediaStream* stream = FindFirstVideoStream(demuxer.streams()); if (!language_override.empty()) { stream->info()->set_language(language_override); ASSERT_EQ(language_override, stream->info()->language()); } muxer_video->AddStream(stream); if (enable_encryption) { muxer_video->SetKeySource(encryption_key_source.get(), KeySource::TRACK_TYPE_SD, kClearLeadInSeconds, kCryptoDurationInSeconds, FOURCC_cenc); } } scoped_ptr muxer_audio; if (!audio_output.empty()) { muxer_audio.reset( new mp4::MP4Muxer(SetupOptions(audio_output, single_segment))); muxer_audio->set_clock(&fake_clock_); MediaStream* stream = FindFirstAudioStream(demuxer.streams()); if (!language_override.empty()) { stream->info()->set_language(language_override); ASSERT_EQ(language_override, stream->info()->language()); } muxer_audio->AddStream(stream); if (enable_encryption) { muxer_audio->SetKeySource(encryption_key_source.get(), KeySource::TRACK_TYPE_SD, kClearLeadInSeconds, kCryptoDurationInSeconds, FOURCC_cenc); } } // Start remuxing process. ASSERT_OK(demuxer.Run()); } void PackagerTestBasic::Decrypt(const std::string& input, const std::string& video_output, const std::string& audio_output) { CHECK(!video_output.empty() || !audio_output.empty()); Demuxer demuxer(GetFullPath(input)); scoped_ptr decryption_key_source( FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); ASSERT_TRUE(decryption_key_source); demuxer.SetKeySource(decryption_key_source.Pass()); ASSERT_OK(demuxer.Initialize()); scoped_ptr muxer; MediaStream* stream(NULL); if (!video_output.empty()) { muxer.reset( new mp4::MP4Muxer(SetupOptions(video_output, true))); stream = FindFirstVideoStream(demuxer.streams()); } if (!audio_output.empty()) { muxer.reset( new mp4::MP4Muxer(SetupOptions(audio_output, true))); stream = FindFirstAudioStream(demuxer.streams()); } ASSERT_TRUE(muxer); ASSERT_TRUE(stream != NULL); ASSERT_TRUE(stream->info()->is_encrypted()); muxer->set_clock(&fake_clock_); muxer->AddStream(stream); ASSERT_OK(demuxer.Run()); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputVideo, kOutputNone, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputVideo, kOutputNone, kSingleSegment, kEnableEncryption, kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputVideo, kOutputVideo2, kOutputNone)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, kEnableEncryption, kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputAudio, kOutputNone, kOutputAudio2)); } TEST_P(PackagerTestBasic, MP4MuxerLanguageWithoutSubtag) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, kDisableEncryption, "por")); Demuxer demuxer(GetFullPath(kOutputAudio)); ASSERT_OK(demuxer.Initialize()); MediaStream* stream = FindFirstAudioStream(demuxer.streams()); ASSERT_EQ("por", stream->info()->language()); } TEST_P(PackagerTestBasic, MP4MuxerLanguageWithSubtag) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, kDisableEncryption, "por-BR")); Demuxer demuxer(GetFullPath(kOutputAudio)); ASSERT_OK(demuxer.Initialize()); MediaStream* stream = FindFirstAudioStream(demuxer.streams()); ASSERT_EQ("por", stream->info()->language()); } class PackagerTest : public PackagerTestBasic { public: void SetUp() override { PackagerTestBasic::SetUp(); ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputVideo, kOutputNone, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); } }; TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedVideoAgain) { // Take the muxer output and feed into muxer again. The new muxer output // should contain the same contents as the previous muxer output. ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideo, kOutputVideo2, kOutputNone, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); } TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedAudioAgain) { // Take the muxer output and feed into muxer again. The new muxer output // should contain the same contents as the previous muxer output. ASSERT_NO_FATAL_FAILURE(Remux(kOutputAudio, kOutputNone, kOutputAudio2, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); } TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputVideo2, kOutputAudio2, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); // Compare the output with single muxer output. They should match. EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); } TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputVideo2, kOutputNone, kMultipleSegments, kDisableEncryption, kNoLanguageOverride)); // Find and concatenates the segments. const std::string kOutputVideoSegmentsCombined = std::string(kOutputVideo) + "_combined"; base::FilePath output_path = test_directory_.AppendASCII(kOutputVideoSegmentsCombined); ASSERT_TRUE( base::CopyFile(test_directory_.AppendASCII(kOutputVideo2), output_path)); const int kStartSegmentIndex = 1; // start from one. int segment_index = kStartSegmentIndex; while (true) { base::FilePath segment_path = test_directory_.AppendASCII( base::StringPrintf(kSegmentTemplateOutputPattern, segment_index)); if (!base::PathExists(segment_path)) break; std::string segment_content; ASSERT_TRUE(base::ReadFileToString(segment_path, &segment_content)); EXPECT_TRUE(base::AppendToFile(output_path, segment_content.data(), segment_content.size())); ++segment_index; } // We should have at least one segment. ASSERT_LT(kStartSegmentIndex, segment_index); // Feed the combined file into muxer again. The new muxer output should be // the same as by just feeding the input to muxer. ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideoSegmentsCombined, kOutputVideo2, kOutputNone, kSingleSegment, kDisableEncryption, kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); } INSTANTIATE_TEST_CASE_P(PackagerEndToEnd, PackagerTestBasic, ValuesIn(kMediaFiles)); INSTANTIATE_TEST_CASE_P(PackagerEndToEnd, PackagerTest, ValuesIn(kMediaFiles)); } // namespace media } // namespace shaka