Support I-Frames only playlist in MediaPlaylist

Issue: #287

Change-Id: I204774fd87cf9a159a4448ba3457db5ab17f7889
This commit is contained in:
KongQun Yang 2018-01-29 19:17:21 -08:00
parent 4194f0746e
commit 6d0a6bb120
5 changed files with 237 additions and 50 deletions

View File

@ -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;
}

View File

@ -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))

View File

@ -59,9 +59,11 @@ std::string CreateExtXMap(const MediaInfo& media_info) {
return ext_x_map;
}
std::string CreatePlaylistHeader(const MediaInfo& media_info,
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();
@ -100,6 +102,10 @@ std::string CreatePlaylistHeader(const MediaInfo& media_info,
default:
NOTREACHED() << "Unexpected MediaPlaylistType " << static_cast<int>(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.";
entries_.emplace_back(new SegmentInfoEntry(
file_name, 0.0, 0.0, !media_info_.has_segment_template(),
start_byte_offset, size, previous_segment_end_offset_));
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);
}
const double start_time_seconds =
static_cast<double>(start_time) / time_scale_;
const double segment_duration_seconds =
static_cast<double>(duration) / time_scale_;
if (segment_duration_seconds > longest_segment_duration_)
longest_segment_duration_ = segment_duration_seconds;
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 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<double>(start_time) / time_scale_;
const double segment_duration_seconds =
static_cast<double>(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 ||

View File

@ -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<std::unique_ptr<HlsEntry>> 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<KeyFrameInfo> key_frames_;
DISALLOW_COPY_AND_ASSIGN(MediaPlaylist);
};

View File

@ -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