HLS MediaPlaylist class
- A class to generate Media Playlists. Issue #85 Change-Id: I689e97c767049bc21de279c743cbabf4ca4711be
This commit is contained in:
parent
5fc09763ce
commit
2adaf1712d
|
@ -0,0 +1,316 @@
|
||||||
|
// Copyright 2016 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 "packager/hls/base/media_playlist.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "packager/base/logging.h"
|
||||||
|
#include "packager/base/strings/stringprintf.h"
|
||||||
|
#include "packager/media/file/file.h"
|
||||||
|
|
||||||
|
namespace edash_packager {
|
||||||
|
namespace hls {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
uint32_t GetTimeScale(const MediaInfo& media_info) {
|
||||||
|
if (media_info.has_reference_time_scale())
|
||||||
|
return media_info.reference_time_scale();
|
||||||
|
|
||||||
|
if (media_info.has_video_info())
|
||||||
|
return media_info.video_info().time_scale();
|
||||||
|
|
||||||
|
if (media_info.has_audio_info())
|
||||||
|
return media_info.audio_info().time_scale();
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SegmentInfoEntry : public HlsEntry {
|
||||||
|
public:
|
||||||
|
SegmentInfoEntry(const std::string& file_name, double duration);
|
||||||
|
~SegmentInfoEntry() override;
|
||||||
|
|
||||||
|
std::string ToString() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::string file_name_;
|
||||||
|
const double duration_;
|
||||||
|
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(SegmentInfoEntry);
|
||||||
|
};
|
||||||
|
|
||||||
|
SegmentInfoEntry::SegmentInfoEntry(const std::string& file_name,
|
||||||
|
double duration)
|
||||||
|
: HlsEntry(HlsEntry::EntryType::kExtInf),
|
||||||
|
file_name_(file_name),
|
||||||
|
duration_(duration) {}
|
||||||
|
SegmentInfoEntry::~SegmentInfoEntry() {}
|
||||||
|
|
||||||
|
std::string SegmentInfoEntry::ToString() {
|
||||||
|
return base::StringPrintf("#EXTINF:%.3f\n%s\n", duration_,
|
||||||
|
file_name_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
class EncryptionInfoEntry : public HlsEntry {
|
||||||
|
public:
|
||||||
|
EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& iv,
|
||||||
|
const std::string& key_format,
|
||||||
|
const std::string& key_format_versions);
|
||||||
|
|
||||||
|
~EncryptionInfoEntry() override;
|
||||||
|
|
||||||
|
std::string ToString() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MediaPlaylist::EncryptionMethod method_;
|
||||||
|
const std::string url_;
|
||||||
|
const std::string iv_;
|
||||||
|
const std::string key_format_;
|
||||||
|
const std::string key_format_versions_;
|
||||||
|
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(EncryptionInfoEntry);
|
||||||
|
};
|
||||||
|
|
||||||
|
EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& iv,
|
||||||
|
const std::string& key_format,
|
||||||
|
const std::string& key_format_versions)
|
||||||
|
: HlsEntry(HlsEntry::EntryType::kExtKey),
|
||||||
|
method_(method),
|
||||||
|
url_(url),
|
||||||
|
iv_(iv),
|
||||||
|
key_format_(key_format),
|
||||||
|
key_format_versions_(key_format_versions) {}
|
||||||
|
|
||||||
|
EncryptionInfoEntry::~EncryptionInfoEntry() {}
|
||||||
|
|
||||||
|
std::string EncryptionInfoEntry::ToString() {
|
||||||
|
std::string method_attribute;
|
||||||
|
if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) {
|
||||||
|
method_attribute = "METHOD=SAMPLE-AES";
|
||||||
|
} else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) {
|
||||||
|
method_attribute = "METHOD=AES-128";
|
||||||
|
} else {
|
||||||
|
DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone);
|
||||||
|
method_attribute = "METHOD=NONE";
|
||||||
|
}
|
||||||
|
std::string ext_key = "#EXT-X-KEY:" + method_attribute + ",URI=\"" + url_ +
|
||||||
|
"\"";
|
||||||
|
if (!iv_.empty()) {
|
||||||
|
ext_key += ",IV=" + iv_;
|
||||||
|
}
|
||||||
|
if (!key_format_versions_.empty()) {
|
||||||
|
ext_key += ",KEYFORMATVERSIONS=\"" + key_format_versions_ + "\"";
|
||||||
|
}
|
||||||
|
if (key_format_.empty())
|
||||||
|
return ext_key + "\n";
|
||||||
|
|
||||||
|
return ext_key + ",KEYFORMAT=\"" + key_format_ + "\"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
|
||||||
|
HlsEntry::~HlsEntry() {}
|
||||||
|
|
||||||
|
MediaPlaylist::MediaPlaylist(const std::string& file_name,
|
||||||
|
const std::string& name,
|
||||||
|
const std::string& group_id)
|
||||||
|
: file_name_(file_name), name_(name), group_id_(group_id) {}
|
||||||
|
MediaPlaylist::~MediaPlaylist() {}
|
||||||
|
|
||||||
|
void MediaPlaylist::SetTypeForTesting(MediaPlaylistType type) {
|
||||||
|
type_ = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MediaPlaylist::SetCodecForTesting(const std::string& codec) {
|
||||||
|
codec_ = codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
|
||||||
|
const uint32_t time_scale = GetTimeScale(media_info);
|
||||||
|
if (time_scale == 0) {
|
||||||
|
LOG(ERROR) << "MediaInfo does not contain a valid timescale.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media_info.has_video_info()) {
|
||||||
|
type_ = MediaPlaylistType::kPlayListVideo;
|
||||||
|
codec_ = media_info.video_info().codec();
|
||||||
|
} else if (media_info.has_audio_info()) {
|
||||||
|
type_ = MediaPlaylistType::kPlayListAudio;
|
||||||
|
codec_ = media_info.audio_info().codec();
|
||||||
|
} else {
|
||||||
|
NOTIMPLEMENTED();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_scale_ = time_scale;
|
||||||
|
media_info_ = media_info;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MediaPlaylist::AddSegment(const std::string& file_name,
|
||||||
|
uint64_t duration,
|
||||||
|
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.";
|
||||||
|
|
||||||
|
scoped_ptr<SegmentInfoEntry> info(new SegmentInfoEntry(file_name, 0.0));
|
||||||
|
entries_.push_back(info.Pass());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double segment_duration = static_cast<double>(duration) / time_scale_;
|
||||||
|
if (segment_duration > longest_segment_duration_)
|
||||||
|
longest_segment_duration_ = segment_duration;
|
||||||
|
|
||||||
|
total_duration_in_seconds_ += segment_duration;
|
||||||
|
total_segments_size_ += size;
|
||||||
|
++total_num_segments_;
|
||||||
|
|
||||||
|
scoped_ptr<SegmentInfoEntry> info(
|
||||||
|
new SegmentInfoEntry(file_name, segment_duration));
|
||||||
|
entries_.push_back(info.Pass());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(rkuroiwa): This works for single key format but won't work for multiple
|
||||||
|
// key formats (e.g. different DRM systems).
|
||||||
|
// Candidate algorithm:
|
||||||
|
// Assume entries_ is std::list (static_assert below).
|
||||||
|
// Create a map from key_format to EncryptionInfoEntry (iterator actually).
|
||||||
|
// Iterate over entries_ until it hits SegmentInfoEntry. While iterating over
|
||||||
|
// entries_ if there are multiple EncryptionInfoEntry with the same key_format,
|
||||||
|
// erase the older ones using the iterator.
|
||||||
|
// Note that when erasing std::list iterators, only the deleted iterators are
|
||||||
|
// invalidated.
|
||||||
|
void MediaPlaylist::RemoveOldestSegment() {
|
||||||
|
static_assert(
|
||||||
|
std::is_same<decltype(entries_), std::list<scoped_ptr<HlsEntry>>>::value,
|
||||||
|
"This algorithm assumes std::list.");
|
||||||
|
if (entries_.empty())
|
||||||
|
return;
|
||||||
|
if (entries_.front()->type() == HlsEntry::EntryType::kExtInf) {
|
||||||
|
entries_.pop_front();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the first EXT-X-KEY entry doesn't get popped out until the
|
||||||
|
// next EXT-X-KEY entry because the first EXT-X-KEY applies to all the
|
||||||
|
// segments following until the next one.
|
||||||
|
|
||||||
|
if (entries_.size() == 1) {
|
||||||
|
// More segments might get added, leave the entry in.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries_.size() == 2) {
|
||||||
|
auto entries_itr = entries_.begin();
|
||||||
|
++entries_itr;
|
||||||
|
if ((*entries_itr)->type() == HlsEntry::EntryType::kExtKey) {
|
||||||
|
entries_.pop_front();
|
||||||
|
} else {
|
||||||
|
entries_.erase(entries_itr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entries_itr = entries_.begin();
|
||||||
|
++entries_itr;
|
||||||
|
if ((*entries_itr)->type() == HlsEntry::EntryType::kExtInf) {
|
||||||
|
DCHECK((*entries_itr)->type() == HlsEntry::EntryType::kExtInf);
|
||||||
|
entries_.erase(entries_itr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
++entries_itr;
|
||||||
|
// This assumes that there is a segment between 2 EXT-X-KEY entries.
|
||||||
|
// Which should be the case due to logic in AddEncryptionInfo().
|
||||||
|
DCHECK((*entries_itr)->type() == HlsEntry::EntryType::kExtInf);
|
||||||
|
entries_.erase(entries_itr);
|
||||||
|
entries_.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MediaPlaylist::AddEncryptionInfo(MediaPlaylist::EncryptionMethod method,
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& iv,
|
||||||
|
const std::string& key_format,
|
||||||
|
const std::string& key_format_versions) {
|
||||||
|
if (!entries_.empty()) {
|
||||||
|
// No reason to have two consecutive EXT-X-KEY entries. Remove the previous
|
||||||
|
// one.
|
||||||
|
if (entries_.back()->type() == HlsEntry::EntryType::kExtKey)
|
||||||
|
entries_.pop_back();
|
||||||
|
}
|
||||||
|
scoped_ptr<EncryptionInfoEntry> info(new EncryptionInfoEntry(
|
||||||
|
method, url, iv, key_format, key_format_versions));
|
||||||
|
entries_.push_back(info.Pass());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MediaPlaylist::WriteToFile(media::File* file) {
|
||||||
|
if (!target_duration_set_) {
|
||||||
|
SetTargetDuration(ceil(GetLongestSegmentDuration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string header = base::StringPrintf("#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:%d\n",
|
||||||
|
target_duration_);
|
||||||
|
std::string body;
|
||||||
|
for (const auto& entry : entries_) {
|
||||||
|
body.append(entry->ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content = header + body;
|
||||||
|
int64_t bytes_written = file->Write(content.data(), content.size());
|
||||||
|
if (bytes_written < 0) {
|
||||||
|
LOG(ERROR) << "Error while writing playlist to file.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(rkuroiwa): There are at least 2 while (remaining_bytes > 0) logic in
|
||||||
|
// this library to handle partial writes by File. Dedup them and use it here
|
||||||
|
// has well.
|
||||||
|
if (static_cast<size_t>(bytes_written) < content.size()) {
|
||||||
|
LOG(ERROR) << "Failed to write the whole playlist. Wrote " << bytes_written
|
||||||
|
<< " but the playlist is " << content.size() << " bytes.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t MediaPlaylist::Bitrate() const {
|
||||||
|
if (media_info_.has_bandwidth())
|
||||||
|
return media_info_.bandwidth();
|
||||||
|
if (total_duration_in_seconds_ == 0.0)
|
||||||
|
return 0;
|
||||||
|
if (total_segments_size_ == 0)
|
||||||
|
return 0;
|
||||||
|
return total_segments_size_ / total_duration_in_seconds_;
|
||||||
|
}
|
||||||
|
|
||||||
|
double MediaPlaylist::GetLongestSegmentDuration() {
|
||||||
|
return longest_segment_duration_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MediaPlaylist::SetTargetDuration(uint32_t target_duration) {
|
||||||
|
if (target_duration_set_) {
|
||||||
|
LOG(WARNING) << "Cannot set target duration to " << target_duration
|
||||||
|
<< ". Target duration already set to " << target_duration_;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
target_duration_ = target_duration;
|
||||||
|
target_duration_set_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace hls
|
||||||
|
} // namespace edash_packager
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright 2016 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
|
||||||
|
|
||||||
|
#ifndef PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_
|
||||||
|
#define PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "packager/base/macros.h"
|
||||||
|
#include "packager/base/memory/scoped_ptr.h"
|
||||||
|
#include "packager/mpd/base/media_info.pb.h"
|
||||||
|
|
||||||
|
namespace edash_packager {
|
||||||
|
|
||||||
|
namespace media {
|
||||||
|
class File;
|
||||||
|
} // namespace media
|
||||||
|
|
||||||
|
namespace hls {
|
||||||
|
|
||||||
|
class HlsEntry {
|
||||||
|
public:
|
||||||
|
enum class EntryType {
|
||||||
|
kExtInf,
|
||||||
|
kExtKey,
|
||||||
|
};
|
||||||
|
virtual ~HlsEntry();
|
||||||
|
|
||||||
|
EntryType type() const { return type_; }
|
||||||
|
virtual std::string ToString() = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
explicit HlsEntry(EntryType type);
|
||||||
|
|
||||||
|
private:
|
||||||
|
EntryType type_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Methods are virtual for mocking.
|
||||||
|
class MediaPlaylist {
|
||||||
|
public:
|
||||||
|
enum class MediaPlaylistType {
|
||||||
|
kPlaylistUnknown,
|
||||||
|
kPlayListAudio,
|
||||||
|
kPlayListVideo,
|
||||||
|
kPlayListSubtitle,
|
||||||
|
};
|
||||||
|
enum class EncryptionMethod {
|
||||||
|
kNone, // No encryption, i.e. clear.
|
||||||
|
kAes128, // Completely encrypted using AES-CBC.
|
||||||
|
kSampleAes, // Encrypted using Sample AES method.
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @param file_name is the file name of this media playlist.
|
||||||
|
/// @param name is the name of this playlist. In other words this is the
|
||||||
|
/// value of the NAME attribute for EXT-X-MEDIA. This is not
|
||||||
|
/// necessarily the same as @a file_name.
|
||||||
|
/// @param group_id is the group ID for this playlist. This is the value of
|
||||||
|
/// GROUP-ID attribute for EXT-X-MEDIA.
|
||||||
|
MediaPlaylist(
|
||||||
|
const std::string& file_name,
|
||||||
|
const std::string& name,
|
||||||
|
const std::string& group_id);
|
||||||
|
virtual ~MediaPlaylist();
|
||||||
|
|
||||||
|
const std::string& file_name() const { return file_name_; }
|
||||||
|
const std::string& name() const { return name_; }
|
||||||
|
const std::string& group_id() const { return group_id_; }
|
||||||
|
MediaPlaylistType type() const { return type_; }
|
||||||
|
const std::string& codec() const { return codec_; }
|
||||||
|
|
||||||
|
/// For testing only.
|
||||||
|
void SetTypeForTesting(MediaPlaylistType type);
|
||||||
|
|
||||||
|
/// For testing only.
|
||||||
|
void SetCodecForTesting(const std::string& codec);
|
||||||
|
|
||||||
|
/// This must succeed before calling any other public methods.
|
||||||
|
/// @param media_info is the info of the segments that are going to be added
|
||||||
|
/// to this playlist.
|
||||||
|
/// @return true on success, false otherwise.
|
||||||
|
virtual bool SetMediaInfo(const MediaInfo& media_info);
|
||||||
|
|
||||||
|
/// Segments must be added in order.
|
||||||
|
/// @param file_name is the file name of the segment.
|
||||||
|
/// @param duration is in terms of the timescale of the media.
|
||||||
|
/// @param size is size in bytes.
|
||||||
|
virtual void AddSegment(const std::string& file_name,
|
||||||
|
uint64_t duration,
|
||||||
|
uint64_t size);
|
||||||
|
|
||||||
|
/// Removes the oldest segment from the playlist. Useful for manually managing
|
||||||
|
/// the length of the playlist.
|
||||||
|
virtual void RemoveOldestSegment();
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
/// @param url specifies where the key is i.e. the value of the URI attribute.
|
||||||
|
/// @param iv is the initialization vector in human readable format, i.e. the
|
||||||
|
/// value for IV attribute. This may be empty.
|
||||||
|
/// @param key_format is the key format, i.e. the KEYFORMAT value. This may be
|
||||||
|
/// empty.
|
||||||
|
/// @param key_format_versions is the KEYFORMATVERIONS value. This may be
|
||||||
|
/// empty.
|
||||||
|
virtual void AddEncryptionInfo(EncryptionMethod method,
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& iv,
|
||||||
|
const std::string& key_format,
|
||||||
|
const std::string& key_format_versions);
|
||||||
|
|
||||||
|
/// Write the playlist to |file|.
|
||||||
|
/// This does not close the file.
|
||||||
|
/// If target duration is not set expliticly, this will try to find the target
|
||||||
|
/// duration. Note that target duration cannot be changed. So calling this
|
||||||
|
/// without explicitly setting the target duration and before adding any
|
||||||
|
/// segments will end up setting the target duration to 0 and will always
|
||||||
|
/// generate an invalid playlist.
|
||||||
|
/// @param file is the output file.
|
||||||
|
/// @return true on success, false otherwise.
|
||||||
|
virtual bool WriteToFile(media::File* file);
|
||||||
|
|
||||||
|
/// If bitrate is specified in MediaInfo then it will use that value.
|
||||||
|
/// Otherwise, it is calculated from the duration and the size of the
|
||||||
|
/// segments added to this object.
|
||||||
|
/// @return the bitrate of this MediaPlaylist.
|
||||||
|
virtual uint64_t Bitrate() const;
|
||||||
|
|
||||||
|
/// @return the longest segment’s duration. This will return 0 if no
|
||||||
|
/// segments have been added.
|
||||||
|
virtual double GetLongestSegmentDuration();
|
||||||
|
|
||||||
|
/// Set the target duration of this MediaPlaylist.
|
||||||
|
/// In other words this is the value for EXT-X-TARGETDURATION.
|
||||||
|
/// If this is not called before calling Write(), it will estimate the best
|
||||||
|
/// target duration.
|
||||||
|
/// The spec does not allow changing EXT-X-TARGETDURATION, once Write() is
|
||||||
|
/// called, this will fail.
|
||||||
|
/// @param target_duration is the target duration for this playlist.
|
||||||
|
/// @return true if set, false otherwise.
|
||||||
|
virtual bool SetTargetDuration(uint32_t target_duration);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Mainly for MasterPlaylist to use these values.
|
||||||
|
const std::string file_name_;
|
||||||
|
const std::string name_;
|
||||||
|
const std::string group_id_;
|
||||||
|
MediaInfo media_info_;
|
||||||
|
MediaPlaylistType type_ = MediaPlaylistType::kPlaylistUnknown;
|
||||||
|
std::string codec_;
|
||||||
|
|
||||||
|
double longest_segment_duration_ = 0.0;
|
||||||
|
uint32_t time_scale_ = 0;
|
||||||
|
|
||||||
|
uint64_t total_segments_size_ = 0;
|
||||||
|
double total_duration_in_seconds_ = 0.0;
|
||||||
|
int total_num_segments_;
|
||||||
|
|
||||||
|
// See SetTargetDuration() comments.
|
||||||
|
bool target_duration_set_ = false;
|
||||||
|
uint32_t target_duration_ = 0;
|
||||||
|
|
||||||
|
std::list<scoped_ptr<HlsEntry>> entries_;
|
||||||
|
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(MediaPlaylist);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace hls
|
||||||
|
} // namespace edash_packager
|
||||||
|
|
||||||
|
#endif // PACKAGER_HLS_BASE_MEDIA_PLAYLIST_H_
|
|
@ -0,0 +1,285 @@
|
||||||
|
// Copyright 2016 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 <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "packager/media/file/file.h"
|
||||||
|
#include "packager/hls/base/media_playlist.h"
|
||||||
|
|
||||||
|
namespace edash_packager {
|
||||||
|
namespace hls {
|
||||||
|
|
||||||
|
using ::testing::_;
|
||||||
|
using ::testing::ReturnArg;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const char kDefaultPlaylistFileName[] = "default_playlist.m3u8";
|
||||||
|
|
||||||
|
class MockFile : public media::File {
|
||||||
|
public:
|
||||||
|
MockFile() : File(kDefaultPlaylistFileName) {}
|
||||||
|
MOCK_METHOD0(Close, bool());
|
||||||
|
MOCK_METHOD2(Read, int64_t(void* buffer, uint64_t length));
|
||||||
|
MOCK_METHOD2(Write,int64_t(const void* buffer, uint64_t length));
|
||||||
|
MOCK_METHOD0(Size, int64_t());
|
||||||
|
MOCK_METHOD0(Flush, bool());
|
||||||
|
MOCK_METHOD1(Seek, bool(uint64_t position));
|
||||||
|
MOCK_METHOD1(Tell, bool(uint64_t* position));
|
||||||
|
|
||||||
|
private:
|
||||||
|
MOCK_METHOD0(Open, bool());
|
||||||
|
};
|
||||||
|
|
||||||
|
MATCHER_P(MatchesString, expected_string, "") {
|
||||||
|
const std::string arg_string(static_cast<const char*>(arg));
|
||||||
|
*result_listener << "which is " << arg_string.size()
|
||||||
|
<< " long and the content is " << arg_string;
|
||||||
|
return expected_string == std::string(static_cast<const char*>(arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class MediaPlaylistTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
MediaPlaylistTest()
|
||||||
|
: default_file_name_(kDefaultPlaylistFileName),
|
||||||
|
default_name_("default_name"),
|
||||||
|
default_group_id_("default_group_id"),
|
||||||
|
media_playlist_(default_file_name_, default_name_, default_group_id_) {}
|
||||||
|
|
||||||
|
void SetUp() override {
|
||||||
|
MediaInfo::VideoInfo* video_info =
|
||||||
|
valid_video_media_info_.mutable_video_info();
|
||||||
|
video_info->set_codec("avc1");
|
||||||
|
video_info->set_time_scale(90000);
|
||||||
|
video_info->set_frame_duration(3000);
|
||||||
|
video_info->set_width(1280);
|
||||||
|
video_info->set_height(720);
|
||||||
|
video_info->set_pixel_width(1);
|
||||||
|
video_info->set_pixel_height(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string default_file_name_;
|
||||||
|
const std::string default_name_;
|
||||||
|
const std::string default_group_id_;
|
||||||
|
MediaPlaylist media_playlist_;
|
||||||
|
|
||||||
|
MediaInfo valid_video_media_info_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify that SetMediaInfo() fails if timescale is not present.
|
||||||
|
TEST_F(MediaPlaylistTest, NoTimeScale) {
|
||||||
|
MediaInfo media_info;
|
||||||
|
EXPECT_FALSE(media_playlist_.SetMediaInfo(media_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current implementation only handles video and audio.
|
||||||
|
TEST_F(MediaPlaylistTest, NoAudioOrVideo) {
|
||||||
|
MediaInfo media_info;
|
||||||
|
media_info.set_reference_time_scale(90000);
|
||||||
|
MediaInfo::TextInfo* text_info = media_info.mutable_text_info();
|
||||||
|
text_info->set_format("vtt");
|
||||||
|
EXPECT_FALSE(media_playlist_.SetMediaInfo(media_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, SetMediaInfo) {
|
||||||
|
MediaInfo media_info;
|
||||||
|
media_info.set_reference_time_scale(90000);
|
||||||
|
MediaInfo::VideoInfo* video_info = media_info.mutable_video_info();
|
||||||
|
video_info->set_width(1280);
|
||||||
|
video_info->set_height(720);
|
||||||
|
EXPECT_TRUE(media_playlist_.SetMediaInfo(media_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that AddSegment works (not crash).
|
||||||
|
TEST_F(MediaPlaylistTest, AddSegment) {
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that AddEncryptionInfo works (not crash).
|
||||||
|
TEST_F(MediaPlaylistTest, AddEncryptionInfo) {
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
|
"http://example.com", "0xabcedf", "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, WriteToFile) {
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:0\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If bitrate (bandwidth) is not set in the MediaInfo, use it.
|
||||||
|
TEST_F(MediaPlaylistTest, UseBitrateInMediaInfo) {
|
||||||
|
valid_video_media_info_.set_bandwidth(8191);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
EXPECT_EQ(8191u, media_playlist_.Bitrate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If bitrate (bandwidth) is not set in the MediaInfo, then calculate from the
|
||||||
|
// segments.
|
||||||
|
TEST_F(MediaPlaylistTest, GetBitrateFromSegments) {
|
||||||
|
valid_video_media_info_.clear_bandwidth();
|
||||||
|
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
// 10 seconds, 1MB.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 20 seconds, 5MB.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 1800000, 5000000);
|
||||||
|
|
||||||
|
// 200KB per second.
|
||||||
|
EXPECT_EQ(200000u, media_playlist_.Bitrate());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, GetLongestSegmentDuration) {
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
// 10 seconds.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 30 seconds.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
|
||||||
|
// 14 seconds.
|
||||||
|
media_playlist_.AddSegment("file3.ts", 1260000, 3000000);
|
||||||
|
|
||||||
|
EXPECT_NEAR(30.0, media_playlist_.GetLongestSegmentDuration(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, SetTargetDuration) {
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
EXPECT_TRUE(media_playlist_.SetTargetDuration(20));
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:20\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
|
||||||
|
// Cannot set target duration more than once.
|
||||||
|
EXPECT_FALSE(media_playlist_.SetTargetDuration(20));
|
||||||
|
EXPECT_FALSE(media_playlist_.SetTargetDuration(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, WriteToFileWithSegments) {
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
// 10 seconds.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 30 seconds.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:30\n"
|
||||||
|
"#EXTINF:10.000\n"
|
||||||
|
"file1.ts\n"
|
||||||
|
"#EXTINF:30.000\n"
|
||||||
|
"file2.ts\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfo) {
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
|
"http://example.com", "0x12345678",
|
||||||
|
"com.widevine", "1/2/4");
|
||||||
|
// 10 seconds.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 30 seconds.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:30\n"
|
||||||
|
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
||||||
|
"URI=\"http://example.com\",IV=0x12345678,KEYFORMATVERSIONS=\"1/2/4\","
|
||||||
|
"KEYFORMAT=\"com.widevine\"\n"
|
||||||
|
"#EXTINF:10.000\n"
|
||||||
|
"file1.ts\n"
|
||||||
|
"#EXTINF:30.000\n"
|
||||||
|
"file2.ts\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, WriteToFileWithEncryptionInfoEmptyIv) {
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
media_playlist_.AddEncryptionInfo(MediaPlaylist::EncryptionMethod::kSampleAes,
|
||||||
|
"http://example.com", "", "com.widevine",
|
||||||
|
"");
|
||||||
|
// 10 seconds.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 30 seconds.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:30\n"
|
||||||
|
"#EXT-X-KEY:METHOD=SAMPLE-AES,"
|
||||||
|
"URI=\"http://example.com\",KEYFORMAT=\"com.widevine\"\n"
|
||||||
|
"#EXTINF:10.000\n"
|
||||||
|
"file1.ts\n"
|
||||||
|
"#EXTINF:30.000\n"
|
||||||
|
"file2.ts\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MediaPlaylistTest, RemoveOldestSegment) {
|
||||||
|
valid_video_media_info_.set_reference_time_scale(90000);
|
||||||
|
ASSERT_TRUE(media_playlist_.SetMediaInfo(valid_video_media_info_));
|
||||||
|
|
||||||
|
// 10 seconds.
|
||||||
|
media_playlist_.AddSegment("file1.ts", 900000, 1000000);
|
||||||
|
// 30 seconds.
|
||||||
|
media_playlist_.AddSegment("file2.ts", 2700000, 5000000);
|
||||||
|
media_playlist_.RemoveOldestSegment();
|
||||||
|
|
||||||
|
const std::string kExpectedOutput =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
"#EXT-X-TARGETDURATION:30\n"
|
||||||
|
"#EXTINF:30.000\n"
|
||||||
|
"file2.ts\n";
|
||||||
|
|
||||||
|
MockFile file;
|
||||||
|
EXPECT_CALL(file,
|
||||||
|
Write(MatchesString(kExpectedOutput), kExpectedOutput.size()))
|
||||||
|
.WillOnce(ReturnArg<1>());
|
||||||
|
EXPECT_TRUE(media_playlist_.WriteToFile(&file));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace hls
|
||||||
|
} // namespace edash_packager
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright 2016 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
|
||||||
|
|
||||||
|
{
|
||||||
|
'includes': [
|
||||||
|
'../common.gypi',
|
||||||
|
],
|
||||||
|
'targets': [
|
||||||
|
{
|
||||||
|
'target_name': 'hls_builder',
|
||||||
|
'type': '<(component)',
|
||||||
|
'sources': [
|
||||||
|
'base/media_playlist.cc',
|
||||||
|
'base/media_playlist.h',
|
||||||
|
],
|
||||||
|
'dependencies': [
|
||||||
|
'../base/base.gyp:base',
|
||||||
|
'../media/file/file.gyp:file',
|
||||||
|
'../mpd/mpd.gyp:media_info_proto',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'target_name': 'hls_unittest',
|
||||||
|
'type': '<(gtest_target_type)',
|
||||||
|
'sources': [
|
||||||
|
'base/media_playlist_unittest.cc',
|
||||||
|
],
|
||||||
|
'dependencies': [
|
||||||
|
'../base/base.gyp:base',
|
||||||
|
'../media/test/media_test.gyp:run_tests_with_atexit_manager',
|
||||||
|
'../mpd/mpd.gyp:media_info_proto',
|
||||||
|
'../testing/gmock.gyp:gmock',
|
||||||
|
'../testing/gtest.gyp:gtest',
|
||||||
|
'hls_builder',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
|
@ -34,6 +34,7 @@
|
||||||
'app/widevine_encryption_flags.h',
|
'app/widevine_encryption_flags.h',
|
||||||
],
|
],
|
||||||
'dependencies': [
|
'dependencies': [
|
||||||
|
'hls/hls.gyp:hls_builder',
|
||||||
'media/event/media_event.gyp:media_event',
|
'media/event/media_event.gyp:media_event',
|
||||||
'media/file/file.gyp:file',
|
'media/file/file.gyp:file',
|
||||||
'media/filters/filters.gyp:filters',
|
'media/filters/filters.gyp:filters',
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
'target_name': 'packager_builder_tests',
|
'target_name': 'packager_builder_tests',
|
||||||
'type': 'none',
|
'type': 'none',
|
||||||
'dependencies': [
|
'dependencies': [
|
||||||
|
'hls/hls.gyp:hls_unittest',
|
||||||
'media/base/media_base.gyp:media_base_unittest',
|
'media/base/media_base.gyp:media_base_unittest',
|
||||||
'media/event/media_event.gyp:media_event_unittest',
|
'media/event/media_event.gyp:media_event_unittest',
|
||||||
'media/file/file.gyp:file_unittest',
|
'media/file/file.gyp:file_unittest',
|
||||||
|
|
Loading…
Reference in New Issue