diff --git a/packager/hls/base/master_playlist.cc b/packager/hls/base/master_playlist.cc index efe33fdd71..6ceebf6fb8 100644 --- a/packager/hls/base/master_playlist.cc +++ b/packager/hls/base/master_playlist.cc @@ -76,12 +76,12 @@ MasterPlaylist::~MasterPlaylist() {} void MasterPlaylist::AddMediaPlaylist(MediaPlaylist* media_playlist) { DCHECK(media_playlist); switch (media_playlist->stream_type()) { - case MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio: { + case MediaPlaylist::MediaPlaylistStreamType::kAudio: { const std::string& group_id = media_playlist->group_id(); audio_playlist_groups_[group_id].push_back(media_playlist); break; } - case MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo: { + case MediaPlaylist::MediaPlaylistStreamType::kVideo: { video_playlists_.push_back(media_playlist); break; } diff --git a/packager/hls/base/master_playlist_unittest.cc b/packager/hls/base/master_playlist_unittest.cc index a274aa2ef2..3ad8f59cb5 100644 --- a/packager/hls/base/master_playlist_unittest.cc +++ b/packager/hls/base/master_playlist_unittest.cc @@ -64,7 +64,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistOneVideo) { MockMediaPlaylist mock_playlist(kVodPlaylist, "media1.m3u8", "somename", "somegroupid"); mock_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo); + MediaPlaylist::MediaPlaylistStreamType::kVideo); mock_playlist.SetCodecForTesting(codec); EXPECT_CALL(mock_playlist, Bitrate()).WillOnce(Return(435889)); EXPECT_CALL(mock_playlist, GetDisplayResolution(NotNull(), NotNull())) @@ -95,7 +95,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndAudio) { MockMediaPlaylist sd_video_playlist(kVodPlaylist, "sd.m3u8", "somename", "somegroupid"); sd_video_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo); + MediaPlaylist::MediaPlaylistStreamType::kVideo); sd_video_playlist.SetCodecForTesting(sd_video_codec); EXPECT_CALL(sd_video_playlist, Bitrate()) .Times(AtLeast(1)) @@ -111,7 +111,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndAudio) { MockMediaPlaylist hd_video_playlist(kVodPlaylist, "hd.m3u8", "somename", "somegroupid"); hd_video_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo); + MediaPlaylist::MediaPlaylistStreamType::kVideo); hd_video_playlist.SetCodecForTesting(hd_video_codec); EXPECT_CALL(hd_video_playlist, Bitrate()) .Times(AtLeast(1)) @@ -131,7 +131,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndAudio) { EXPECT_CALL(english_playlist, GetLanguage()).WillRepeatedly(Return("en")); EXPECT_CALL(english_playlist, GetNumChannels()).WillRepeatedly(Return(2)); english_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); english_playlist.SetCodecForTesting(audio_codec); EXPECT_CALL(english_playlist, Bitrate()) .Times(AtLeast(1)) @@ -146,7 +146,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndAudio) { EXPECT_CALL(spanish_playlist, GetLanguage()).WillRepeatedly(Return("es")); EXPECT_CALL(spanish_playlist, GetNumChannels()).WillRepeatedly(Return(5)); spanish_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); spanish_playlist.SetCodecForTesting(audio_codec); EXPECT_CALL(spanish_playlist, Bitrate()) .Times(AtLeast(1)) @@ -187,7 +187,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMultipleAudioGroups) { MockMediaPlaylist video_playlist(kVodPlaylist, "video.m3u8", "somename", "somegroupid"); video_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo); + MediaPlaylist::MediaPlaylistStreamType::kVideo); video_playlist.SetCodecForTesting(video_codec); EXPECT_CALL(video_playlist, Bitrate()) .Times(AtLeast(1)) @@ -205,7 +205,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMultipleAudioGroups) { EXPECT_CALL(eng_lo_playlist, GetLanguage()).WillRepeatedly(Return("en")); EXPECT_CALL(eng_lo_playlist, GetNumChannels()).WillRepeatedly(Return(1)); eng_lo_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); eng_lo_playlist.SetCodecForTesting(audio_codec_lo); EXPECT_CALL(eng_lo_playlist, Bitrate()) .Times(AtLeast(1)) @@ -221,7 +221,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMultipleAudioGroups) { EXPECT_CALL(eng_hi_playlist, GetLanguage()).WillRepeatedly(Return("en")); EXPECT_CALL(eng_hi_playlist, GetNumChannels()).WillRepeatedly(Return(8)); eng_hi_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); eng_hi_playlist.SetCodecForTesting(audio_codec_hi); EXPECT_CALL(eng_hi_playlist, Bitrate()) .Times(AtLeast(1)) @@ -262,7 +262,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistSameAudioGroupSameLanguage) { MockMediaPlaylist video_playlist(kVodPlaylist, "video.m3u8", "somename", "somegroupid"); video_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListVideo); + MediaPlaylist::MediaPlaylistStreamType::kVideo); video_playlist.SetCodecForTesting(video_codec); EXPECT_CALL(video_playlist, Bitrate()) .Times(AtLeast(1)) @@ -279,7 +279,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistSameAudioGroupSameLanguage) { EXPECT_CALL(eng_lo_playlist, GetLanguage()).WillRepeatedly(Return("en")); EXPECT_CALL(eng_lo_playlist, GetNumChannels()).WillRepeatedly(Return(1)); eng_lo_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); eng_lo_playlist.SetCodecForTesting(audio_codec); EXPECT_CALL(eng_lo_playlist, Bitrate()) .Times(AtLeast(1)) @@ -294,7 +294,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistSameAudioGroupSameLanguage) { EXPECT_CALL(eng_hi_playlist, GetLanguage()).WillRepeatedly(Return("en")); EXPECT_CALL(eng_hi_playlist, GetNumChannels()).WillRepeatedly(Return(8)); eng_hi_playlist.SetStreamTypeForTesting( - MediaPlaylist::MediaPlaylistStreamType::kPlayListAudio); + MediaPlaylist::MediaPlaylistStreamType::kAudio); eng_hi_playlist.SetCodecForTesting(audio_codec); EXPECT_CALL(eng_hi_playlist, Bitrate()) .Times(AtLeast(1)) diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index 11698dc8dc..603ae9aaa8 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -59,11 +59,13 @@ std::string CreateExtXMap(const MediaInfo& media_info) { return ext_x_map; } -std::string CreatePlaylistHeader(const MediaInfo& media_info, - uint32_t target_duration, - HlsPlaylistType type, - int media_sequence_number, - int discontinuity_sequence_number) { +std::string CreatePlaylistHeader( + const MediaInfo& media_info, + uint32_t target_duration, + HlsPlaylistType type, + MediaPlaylist::MediaPlaylistStreamType stream_type, + int media_sequence_number, + int discontinuity_sequence_number) { const std::string version = GetPackagerVersion(); std::string version_line; if (!version.empty()) { @@ -100,6 +102,10 @@ std::string CreatePlaylistHeader(const MediaInfo& media_info, default: NOTREACHED() << "Unexpected MediaPlaylistType " << static_cast(type); } + if (stream_type == + MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) { + base::StringAppendF(&header, "#EXT-X-I-FRAMES-ONLY\n"); + } // Put EXT-X-MAP at the end since the rest of the playlist is about the // segment and key info. @@ -319,10 +325,10 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) { } if (media_info.has_video_info()) { - stream_type_ = MediaPlaylistStreamType::kPlayListVideo; + stream_type_ = MediaPlaylistStreamType::kVideo; codec_ = media_info.video_info().codec(); } else if (media_info.has_audio_info()) { - stream_type_ = MediaPlaylistStreamType::kPlayListAudio; + stream_type_ = MediaPlaylistStreamType::kAudio; codec_ = media_info.audio_info().codec(); } else { NOTIMPLEMENTED(); @@ -331,6 +337,7 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) { time_scale_ = time_scale; media_info_ = media_info; + use_byte_range_ = !media_info_.has_segment_template(); return true; } @@ -339,32 +346,45 @@ void MediaPlaylist::AddSegment(const std::string& file_name, uint64_t duration, uint64_t start_byte_offset, uint64_t size) { - if (time_scale_ == 0) { - LOG(WARNING) << "Timescale is not set and the duration for " << duration - << " cannot be calculated. The output will be wrong."; + if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly) { + if (key_frames_.empty()) + return; + // Skip the last entry as the duration of the key frames are defined by the + // next key frame, which we don't know yet. + for (auto iter = key_frames_.begin(); iter != std::prev(key_frames_.end()); + ++iter) { + const std::string& segment_file_name = + iter->segment_file_name.empty() ? file_name : iter->segment_file_name; + AddSegmentInfoEntry(segment_file_name, iter->timestamp, iter->duration, + iter->start_byte_offset, iter->size); + } - entries_.emplace_back(new SegmentInfoEntry( - file_name, 0.0, 0.0, !media_info_.has_segment_template(), - start_byte_offset, size, previous_segment_end_offset_)); + key_frames_.erase(key_frames_.begin(), std::prev(key_frames_.end())); + KeyFrameInfo& key_frame = key_frames_.front(); + key_frame.segment_file_name = file_name; + key_frame.duration = start_time + duration - key_frame.timestamp; return; } + return AddSegmentInfoEntry(file_name, start_time, duration, start_byte_offset, + size); +} - const double start_time_seconds = - static_cast(start_time) / time_scale_; - const double segment_duration_seconds = - static_cast(duration) / time_scale_; - if (segment_duration_seconds > longest_segment_duration_) - longest_segment_duration_ = segment_duration_seconds; - - const int kBitsInByte = 8; - const uint64_t bitrate = kBitsInByte * size / segment_duration_seconds; - max_bitrate_ = std::max(max_bitrate_, bitrate); - entries_.emplace_back(new SegmentInfoEntry( - file_name, start_time_seconds, segment_duration_seconds, - !media_info_.has_segment_template(), start_byte_offset, size, - previous_segment_end_offset_)); - previous_segment_end_offset_ = start_byte_offset + size - 1; - SlideWindow(); +void MediaPlaylist::AddKeyFrame(uint64_t timestamp, + uint64_t start_byte_offset, + uint64_t size) { + if (stream_type_ != MediaPlaylistStreamType::kVideoIFramesOnly) { + if (stream_type_ != MediaPlaylistStreamType::kVideo) { + LOG(WARNING) + << "I-Frames Only playlist applies to video renditions only."; + return; + } + stream_type_ = MediaPlaylistStreamType::kVideoIFramesOnly; + use_byte_range_ = true; + } + if (!key_frames_.empty()) { + key_frames_.back().duration = timestamp - key_frames_.back().timestamp; + } + key_frames_.push_back({timestamp, start_byte_offset, size}); } void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method, @@ -389,13 +409,24 @@ void MediaPlaylist::AddPlacementOpportunity() { } bool MediaPlaylist::WriteToFile(const std::string& file_path) { + if (!key_frames_.empty() && playlist_type_ == HlsPlaylistType::kVod) { + // Flush remaining key frames. This assumes |WriteToFile| is only called + // once at the end of the file in VOD. + CHECK_EQ(key_frames_.size(), 1u); + const KeyFrameInfo& key_frame = key_frames_.front(); + AddSegmentInfoEntry(key_frame.segment_file_name, key_frame.timestamp, + key_frame.duration, key_frame.start_byte_offset, + key_frame.size); + key_frames_.clear(); + } + if (!target_duration_set_) { SetTargetDuration(ceil(GetLongestSegmentDuration())); } std::string header = CreatePlaylistHeader( - media_info_, target_duration_, playlist_type_, media_sequence_number_, - discontinuity_sequence_number_); + media_info_, target_duration_, playlist_type_, stream_type_, + media_sequence_number_, discontinuity_sequence_number_); std::string body; for (const auto& entry : entries_) @@ -472,6 +503,38 @@ bool MediaPlaylist::GetDisplayResolution(uint32_t* width, return false; } +void MediaPlaylist::AddSegmentInfoEntry(const std::string& segment_file_name, + uint64_t start_time, + uint64_t duration, + uint64_t start_byte_offset, + uint64_t size) { + if (time_scale_ == 0) { + LOG(WARNING) << "Timescale is not set and the duration for " << duration + << " cannot be calculated. The output will be wrong."; + + entries_.emplace_back(new SegmentInfoEntry( + segment_file_name, 0.0, 0.0, use_byte_range_, start_byte_offset, size, + previous_segment_end_offset_)); + return; + } + + const double start_time_seconds = + static_cast(start_time) / time_scale_; + const double segment_duration_seconds = + static_cast(duration) / time_scale_; + if (segment_duration_seconds > longest_segment_duration_) + longest_segment_duration_ = segment_duration_seconds; + + const int kBitsInByte = 8; + const uint64_t bitrate = kBitsInByte * size / segment_duration_seconds; + max_bitrate_ = std::max(max_bitrate_, bitrate); + entries_.emplace_back(new SegmentInfoEntry( + segment_file_name, start_time_seconds, segment_duration_seconds, + use_byte_range_, start_byte_offset, size, previous_segment_end_offset_)); + previous_segment_end_offset_ = start_byte_offset + size - 1; + SlideWindow(); +} + void MediaPlaylist::SlideWindow() { DCHECK(!entries_.empty()); if (time_shift_buffer_depth_ <= 0.0 || diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h index 208b39c48f..2f50cd1564 100644 --- a/packager/hls/base/media_playlist.h +++ b/packager/hls/base/media_playlist.h @@ -45,10 +45,11 @@ class HlsEntry { class MediaPlaylist { public: enum class MediaPlaylistStreamType { - kPlaylistUnknown, - kPlayListAudio, - kPlayListVideo, - kPlayListSubtitle, + kUnknown, + kAudio, + kVideo, + kVideoIFramesOnly, + kSubtitle, }; enum class EncryptionMethod { kNone, // No encryption, i.e. clear. @@ -104,6 +105,16 @@ class MediaPlaylist { uint64_t start_byte_offset, uint64_t size); + /// Keyframes must be added in order. It is also called before the containing + /// segment being called. + /// @param timestamp is the timestamp of the key frame in timescale of the + /// media. + /// @param start_byte_offset is the offset of where the key frame starts. + /// @param size is size in bytes. + virtual void AddKeyFrame(uint64_t timestamp, + uint64_t start_byte_offset, + uint64_t size); + /// All segments added after calling this method must be decryptable with /// the key that can be fetched from |url|, until calling this again. /// @param method is the encryption method. @@ -168,6 +179,12 @@ class MediaPlaylist { virtual bool GetDisplayResolution(uint32_t* width, uint32_t* height) const; private: + // Add a SegmentInfoEntry (#EXTINF). + void AddSegmentInfoEntry(const std::string& segment_file_name, + uint64_t start_time, + uint64_t duration, + uint64_t start_byte_offset, + uint64_t size); // Remove elements from |entries_| for live profile. Increments // |sequence_number_| by the number of segments removed. void SlideWindow(); @@ -179,8 +196,9 @@ class MediaPlaylist { const std::string name_; const std::string group_id_; MediaInfo media_info_; - MediaPlaylistStreamType stream_type_ = - MediaPlaylistStreamType::kPlaylistUnknown; + MediaPlaylistStreamType stream_type_ = MediaPlaylistStreamType::kUnknown; + // Whether to use byte range for SegmentInfoEntry. + bool use_byte_range_ = false; std::string codec_; int media_sequence_number_ = 0; bool inserted_discontinuity_tag_ = false; @@ -201,6 +219,16 @@ class MediaPlaylist { std::list> entries_; + // Used by kVideoIFrameOnly playlists to track the i-frames (key frames). + struct KeyFrameInfo { + uint64_t timestamp; + uint64_t start_byte_offset; + uint64_t size; + uint64_t duration; + std::string segment_file_name; + }; + std::list key_frames_; + DISALLOW_COPY_AND_ASSIGN(MediaPlaylist); }; diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc index eff9ebd33a..42dd8835e5 100644 --- a/packager/hls/base/media_playlist_unittest.cc +++ b/packager/hls/base/media_playlist_unittest.cc @@ -771,5 +771,101 @@ TEST_F(EventMediaPlaylistTest, Basic) { ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); } +class IFrameMediaPlaylistTest : public MediaPlaylistTest {}; + +TEST_F(IFrameMediaPlaylistTest, MediaPlaylistType) { + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + EXPECT_EQ(MediaPlaylist::MediaPlaylistStreamType::kVideo, + media_playlist_.stream_type()); + media_playlist_.AddKeyFrame(0, 1000, 2345); + // Playlist stream type is updated to I-Frames only after seeing + // |AddKeyFrame|. + EXPECT_EQ(MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly, + media_playlist_.stream_type()); +} + +TEST_F(IFrameMediaPlaylistTest, SingleSegment) { + valid_video_media_info_.set_media_file_name("file.mp4"); + valid_video_media_info_.mutable_init_range()->set_begin(0); + valid_video_media_info_.mutable_init_range()->set_end(500); + + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + media_playlist_.AddKeyFrame(0, 1000, 2345); + media_playlist_.AddKeyFrame(2 * kTimeScale, 5000, 6345); + media_playlist_.AddSegment("file.mp4", 0, 10 * kTimeScale, kZeroByteOffset, + kMBytes); + media_playlist_.AddKeyFrame(11 * kTimeScale, kMBytes + 1000, 2345); + media_playlist_.AddKeyFrame(15 * kTimeScale, kMBytes + 3345, 12345); + media_playlist_.AddSegment("file.mp4", 10 * kTimeScale, 10 * kTimeScale, + 1001000, 2 * kMBytes); + + const char kExpectedOutput[] = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/google/shaka-packager version " + "test\n" + "#EXT-X-TARGETDURATION:9\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-I-FRAMES-ONLY\n" + "#EXT-X-MAP:URI=\"file.mp4\",BYTERANGE=\"501@0\"\n" + "#EXTINF:2.000,\n" + "#EXT-X-BYTERANGE:2345@1000\n" + "file.mp4\n" + "#EXTINF:9.000,\n" + "#EXT-X-BYTERANGE:6345@5000\n" + "file.mp4\n" + "#EXTINF:4.000,\n" + "#EXT-X-BYTERANGE:2345@1001000\n" + "file.mp4\n" + "#EXTINF:5.000,\n" + "#EXT-X-BYTERANGE:12345\n" + "file.mp4\n" + "#EXT-X-ENDLIST\n"; + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath)); + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + +TEST_F(IFrameMediaPlaylistTest, MultiSegment) { + valid_video_media_info_.set_reference_time_scale(90000); + ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_)); + + media_playlist_.AddKeyFrame(0, 1000, 2345); + media_playlist_.AddKeyFrame(2 * kTimeScale, 5000, 6345); + media_playlist_.AddSegment("file1.ts", 0, 10 * kTimeScale, kZeroByteOffset, + kMBytes); + media_playlist_.AddKeyFrame(11 * kTimeScale, 1000, 2345); + media_playlist_.AddKeyFrame(15 * kTimeScale, 3345, 12345); + media_playlist_.AddSegment("file2.ts", 10 * kTimeScale, 30 * kTimeScale, + kZeroByteOffset, 5 * kMBytes); + + const char kExpectedOutput[] = + "#EXTM3U\n" + "#EXT-X-VERSION:6\n" + "## Generated with https://github.com/google/shaka-packager version " + "test\n" + "#EXT-X-TARGETDURATION:25\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-I-FRAMES-ONLY\n" + "#EXTINF:2.000,\n" + "#EXT-X-BYTERANGE:2345@1000\n" + "file1.ts\n" + "#EXTINF:9.000,\n" + "#EXT-X-BYTERANGE:6345@5000\n" + "file1.ts\n" + "#EXTINF:4.000,\n" + "#EXT-X-BYTERANGE:2345@1000\n" + "file2.ts\n" + "#EXTINF:25.000,\n" + "#EXT-X-BYTERANGE:12345\n" + "file2.ts\n" + "#EXT-X-ENDLIST\n"; + + const char kMemoryFilePath[] = "memory://media.m3u8"; + EXPECT_TRUE(media_playlist_.WriteToFile(kMemoryFilePath)); + ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); +} + } // namespace hls } // namespace shaka