Support HLS characteristics

Add hls_characteristics stream descriptor, which is a colon or semi-colon
separated list of strings. It is optional.

Fixes #430.

Change-Id: Ifcf79316e68768ff065891933de565cd0ff32ec4
This commit is contained in:
KongQun Yang 2018-10-10 15:30:28 -07:00
parent 74df8d30cc
commit 273ab09f05
18 changed files with 156 additions and 21 deletions

View File

@ -24,3 +24,9 @@ HLS specific stream descriptor fields
'.m3u8', and is relative to hls_master_playlist_output (see below). Should
only be set for video streams. If unspecified, no I-Frames only playlist is
created.
:hls_characteristics (charcs):
Optional colon or semi-colon separated list of values for the
CHARACTERISTICS attribute for EXT-X-MEDIA. See CHARACTERISTICS attribute in
http://bit.ly/2OOUkdB for details.

View File

@ -101,7 +101,10 @@ const char kUsage[] =
" - iframe_playlist_name: The optional HLS I-Frames only playlist file\n"
" to create. Usually ends with '.m3u8', and is relative to\n"
" hls_master_playlist_output. Should only be set for video streams. If\n"
" unspecified, no I-Frames only playlist is created.\n";
" unspecified, no I-Frames only playlist is created.\n"
" - hls_characteristics (charcs): Optional colon/semicolon separated\n"
" list of values for the CHARACTERISTICS attribute for EXT-X-MEDIA.\n"
" See CHARACTERISTICS attribute in http://bit.ly/2OOUkdB for details.\n";
// Labels for parameters in RawKey key info.
const char kDrmLabelLabel[] = "label";

View File

@ -30,6 +30,7 @@ enum FieldType {
kTrickPlayFactorField,
kSkipEncryptionField,
kDrmStreamLabelField,
kHlsCharacteristicsField,
};
struct FieldNameToTypeMapping {
@ -63,6 +64,9 @@ const FieldNameToTypeMapping kFieldNameTypeMappings[] = {
{"skip_encryption", kSkipEncryptionField},
{"drm_stream_label", kDrmStreamLabelField},
{"drm_label", kDrmStreamLabelField},
{"hls_characteristics", kHlsCharacteristicsField},
{"characteristics", kHlsCharacteristicsField},
{"charcs", kHlsCharacteristicsField},
};
FieldType GetFieldType(const std::string& field_name) {
@ -164,10 +168,14 @@ base::Optional<StreamDescriptor> ParseStreamDescriptor(
descriptor.skip_encryption = skip_encryption_value > 0;
break;
}
case kDrmStreamLabelField: {
case kDrmStreamLabelField:
descriptor.drm_label = iter->second;
break;
}
case kHlsCharacteristicsField:
descriptor.hls_characteristics =
base::SplitString(iter->second, ";:", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
break;
default:
LOG(ERROR) << "Unknown field in stream descriptor (\"" << iter->first
<< "\").";

View File

@ -271,6 +271,7 @@ class PackagerAppTest(unittest.TestCase):
segmented=False,
using_time_specifier=False,
hls=False,
hls_characteristics=None,
trick_play_factor=None,
drm_label=None,
skip_encryption=None,
@ -290,23 +291,24 @@ class PackagerAppTest(unittest.TestCase):
language: The language override for the input stream.
output_file_prefix: The output file prefix. Default to empty if not
specified.
output_format: Specify the format for the output.
output_format: The format for the output.
segmented: Should the output use a segmented formatted. This will affect
the output extensions and manifests.
using_time_specifier: Use $Time$ in segment name instead of using
$Number$. This flag is only relevant if segmented is True.
hls: Should the output be for an HLS manifest.
hls_characteristics: CHARACTERISTICS attribute for the HLS stream.
trick_play_factor: Signals the stream is to be used for a trick play
stream and which key frames to use. A trick play factor of 0 is the
same as not specifying a trick play factor.
drm_label: Sets the drm label for the stream.
drm_label: The drm label for the stream.
skip_encryption: If set to true, the stream will not be encrypted.
bandwidth: The expected bandwidth value that should be listed in the
manifest.
split_content_on_ad_cues: If set to true, the output file will be split
into multiple files, with a total of NumAdCues + 1 files.
test_file: Specify the input file to use. If the input file is not
specify, a default file will be used.
test_file: The input file to use. If the input file is not specified, a
default file will be used.
Returns:
@ -348,6 +350,9 @@ class PackagerAppTest(unittest.TestCase):
stream.Append('iframe_playlist_name',
output_file_name_base + '-iframe.m3u8')
if hls_characteristics:
stream.Append('hls_characteristics', hls_characteristics)
requires_init_segment = segmented and base_ext not in [
'aac', 'ac3', 'ec3', 'ts', 'vtt'
]
@ -1503,7 +1508,11 @@ class PackagerFunctionalTest(PackagerAppTest):
streams = self._GetStreams(
['audio', 'video'], output_format='ts', segmented=True)
streams += self._GetStreams(
['text'], test_files=['bear-english.vtt'], segmented=True)
['text'],
test_files=['bear-english.vtt'],
segmented=True,
hls_characteristics='public.accessibility.transcribes-spoken-dialog;'
'private.accessibility.widevine-special')
flags = self._GetFlags(output_hls=True)

View File

@ -3,7 +3,7 @@
#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_1.m3u8",GROUP-ID="default-audio-group",NAME="stream_1",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,private.accessibility.widevine-special"
#EXT-X-STREAM-INF:BANDWIDTH=1217518,AVERAGE-BANDWIDTH=1117319,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,AUDIO="default-audio-group",SUBTITLES="default-text-group"
stream_2.m3u8

View File

@ -287,6 +287,12 @@ void BuildMediaTag(const MediaPlaylist& playlist,
tag.AddString("AUTOSELECT", "YES");
}
const std::vector<std::string>& characteristics = playlist.characteristics();
if (!characteristics.empty()) {
tag.AddQuotedString("CHARACTERISTICS",
base::JoinString(characteristics, ","));
}
const MediaPlaylist::MediaPlaylistStreamType kAudio =
MediaPlaylist::MediaPlaylistStreamType::kAudio;
if (playlist.stream_type() == kAudio) {

View File

@ -399,6 +399,42 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideosAndTexts) {
ASSERT_EQ(expected, actual);
}
TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndTextWithCharacteritics) {
// Video, sd.m3u8.
std::unique_ptr<MockMediaPlaylist> video =
CreateVideoPlaylist("sd.m3u8", "sdvideocodec", 300000, 200000);
// Text, eng.m3u8.
std::unique_ptr<MockMediaPlaylist> text =
CreateTextPlaylist("eng.m3u8", "english", "textgroup", "textcodec", "en");
text->SetCharacteristicsForTesting(std::vector<std::string>{
"public.accessibility.transcribes-spoken-dialog", "public.easy-to-read"});
const char kBaseUrl[] = "http://playlists.org/";
EXPECT_TRUE(master_playlist_.WriteMasterPlaylist(kBaseUrl, test_output_dir_,
{video.get(), text.get()}));
std::string actual;
ASSERT_TRUE(File::ReadFileToString(master_playlist_path_.c_str(), &actual));
const std::string expected =
"#EXTM3U\n"
"## Generated with https://github.com/google/shaka-packager version "
"test\n"
"\n"
"#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/eng.m3u8\","
"GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\",DEFAULT=YES,"
"AUTOSELECT=YES,CHARACTERISTICS=\""
"public.accessibility.transcribes-spoken-dialog,public.easy-to-read\"\n"
"\n"
"#EXT-X-STREAM-INF:BANDWIDTH=300000,AVERAGE-BANDWIDTH=200000,"
"CODECS=\"sdvideocodec,textcodec\",RESOLUTION=800x600,"
"SUBTITLES=\"textgroup\"\n"
"http://playlists.org/sd.m3u8\n";
ASSERT_EQ(expected, actual);
}
TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndTextGroups) {
// Video, sd.m3u8.
std::unique_ptr<MockMediaPlaylist> video =

View File

@ -348,6 +348,11 @@ void MediaPlaylist::SetLanguageForTesting(const std::string& language) {
language_ = language;
}
void MediaPlaylist::SetCharacteristicsForTesting(
const std::vector<std::string>& characteristics) {
characteristics_ = characteristics;
}
bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
const uint32_t time_scale = GetTimeScale(media_info);
if (time_scale == 0) {
@ -370,6 +375,9 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
media_info_ = media_info;
language_ = GetLanguage(media_info);
use_byte_range_ = !media_info_.has_segment_template_url();
characteristics_ =
std::vector<std::string>(media_info_.hls_characteristics().begin(),
media_info_.hls_characteristics().end());
return true;
}

View File

@ -10,6 +10,7 @@
#include <list>
#include <memory>
#include <string>
#include <vector>
#include "packager/base/macros.h"
#include "packager/hls/public/hls_params.h"
@ -87,6 +88,10 @@ class MediaPlaylist {
/// For testing only.
void SetLanguageForTesting(const std::string& language);
/// For testing only.
void SetCharacteristicsForTesting(
const std::vector<std::string>& characteristics);
/// 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.
@ -182,7 +187,11 @@ class MediaPlaylist {
/// @return the language of the media, as an ISO language tag in its shortest
/// form. May be an empty string for video.
std::string language() const { return language_; }
const std::string& language() const { return language_; }
const std::vector<std::string>& characteristics() const {
return characteristics_;
}
private:
// Add a SegmentInfoEntry (#EXTINF).
@ -213,6 +222,7 @@ class MediaPlaylist {
bool use_byte_range_ = false;
std::string codec_;
std::string language_;
std::vector<std::string> characteristics_;
int media_sequence_number_ = 0;
bool inserted_discontinuity_tag_ = false;
int discontinuity_sequence_number_ = 0;

View File

@ -18,6 +18,7 @@ namespace shaka {
namespace hls {
using ::testing::_;
using ::testing::ElementsAreArray;
using ::testing::ReturnArg;
namespace {
@ -483,6 +484,20 @@ TEST_F(MediaPlaylistMultiSegmentTest, GetNumChannels) {
EXPECT_EQ(8, media_playlist_->GetNumChannels());
}
TEST_F(MediaPlaylistMultiSegmentTest, Characteristics) {
MediaInfo media_info;
media_info.set_reference_time_scale(kTimeScale);
static const char* kCharacteristics[] = {"some.characteristic",
"another.characteristic"};
media_info.add_hls_characteristics(kCharacteristics[0]);
media_info.add_hls_characteristics(kCharacteristics[1]);
ASSERT_TRUE(media_playlist_->SetMediaInfo(media_info));
EXPECT_THAT(media_playlist_->characteristics(),
ElementsAreArray(kCharacteristics));
}
TEST_F(MediaPlaylistMultiSegmentTest, InitSegment) {
valid_video_media_info_.set_reference_time_scale(90000);
valid_video_media_info_.set_init_segment_url("init_segment.mp4");

View File

@ -21,11 +21,13 @@ HlsNotifyMuxerListener::HlsNotifyMuxerListener(
bool iframes_only,
const std::string& ext_x_media_name,
const std::string& ext_x_media_group_id,
const std::vector<std::string>& characteristics,
hls::HlsNotifier* hls_notifier)
: playlist_name_(playlist_name),
iframes_only_(iframes_only),
ext_x_media_name_(ext_x_media_name),
ext_x_media_group_id_(ext_x_media_group_id),
characteristics_(characteristics),
hls_notifier_(hls_notifier) {
DCHECK(hls_notifier);
}
@ -90,6 +92,10 @@ void HlsNotifyMuxerListener::OnMediaStart(const MuxerOptions& muxer_options,
LOG(ERROR) << "Failed to generate MediaInfo from input.";
return;
}
if (!characteristics_.empty()) {
for (const std::string& characteristic : characteristics_)
media_info->add_hls_characteristics(characteristic);
}
if (protection_scheme_ != FOURCC_NULL) {
internal::SetContentProtectionFields(protection_scheme_, next_key_id_,
next_key_system_infos_,

View File

@ -9,6 +9,7 @@
#include <memory>
#include <string>
#include <vector>
#include "packager/base/optional.h"
#include "packager/media/event/event_info.h"
@ -35,11 +36,15 @@ class HlsNotifyMuxerListener : public MuxerListener {
/// @param ext_x_media_group_id is the group ID for this playlist. This is the
/// value of GROUP-ID attribute for EXT-X-MEDIA. This may be empty for
/// video.
/// @param characteristics is the characteristics for this playlist. This is
/// the value of CHARACTERISTICS attribute for EXT-X-MEDIA. This may be
/// empty.
/// @param hls_notifier used by this listener. Ownership does not transfer.
HlsNotifyMuxerListener(const std::string& playlist_name,
bool iframes_only,
const std::string& ext_x_media_name,
const std::string& ext_x_media_group_id,
const std::vector<std::string>& characteristics,
hls::HlsNotifier* hls_notifier);
~HlsNotifyMuxerListener() override;
@ -77,6 +82,7 @@ class HlsNotifyMuxerListener : public MuxerListener {
const bool iframes_only_;
const std::string ext_x_media_name_;
const std::string ext_x_media_group_id_;
const std::vector<std::string> characteristics_;
hls::HlsNotifier* const hls_notifier_;
base::Optional<uint32_t> stream_id_;

View File

@ -18,7 +18,9 @@ namespace media {
using ::testing::_;
using ::testing::Bool;
using ::testing::ElementsAre;
using ::testing::InSequence;
using ::testing::Property;
using ::testing::Return;
using ::testing::StrEq;
using ::testing::TestWithParam;
@ -92,6 +94,8 @@ const bool kIFramesOnlyPlaylist = true;
const char kDefaultPlaylistName[] = "default_playlist.m3u8";
const char kDefaultName[] = "DEFAULTNAME";
const char kDefaultGroupId[] = "DEFAULTGROUPID";
const char kCharactersticA[] = "public.accessibility.transcribes-spoken-dialog";
const char kCharactersticB[] = "public.easy-to-read";
MATCHER_P(HasEncryptionScheme, expected_scheme, "") {
*result_listener << "it has_protected_content: "
@ -113,6 +117,7 @@ class HlsNotifyMuxerListenerTest : public ::testing::Test {
!kIFramesOnlyPlaylist,
kDefaultName,
kDefaultGroupId,
std::vector<std::string>{kCharactersticA, kCharactersticB},
&mock_notifier_) {}
MuxerListener::MediaRanges GetMediaRanges(
@ -152,9 +157,12 @@ TEST_F(HlsNotifyMuxerListenerTest, OnMediaStart) {
std::shared_ptr<StreamInfo> video_stream_info =
CreateVideoStreamInfo(video_params);
EXPECT_CALL(mock_notifier_,
NotifyNewStream(_, StrEq(kDefaultPlaylistName),
StrEq("DEFAULTNAME"), StrEq("DEFAULTGROUPID"), _))
EXPECT_CALL(
mock_notifier_,
NotifyNewStream(Property(&MediaInfo::hls_characteristics,
ElementsAre(kCharactersticA, kCharactersticB)),
StrEq(kDefaultPlaylistName), StrEq("DEFAULTNAME"),
StrEq("DEFAULTGROUPID"), _))
.WillOnce(Return(true));
MuxerOptions muxer_options;
@ -436,6 +444,7 @@ class HlsNotifyMuxerListenerKeyFrameTest : public TestWithParam<bool> {
GetParam(),
kDefaultName,
kDefaultGroupId,
std::vector<std::string>(), // no characteristics.
&mock_notifier_) {}
MockHlsNotifier mock_notifier_;

View File

@ -44,26 +44,29 @@ std::list<std::unique_ptr<MuxerListener>> CreateHlsListenersInternal(
DCHECK(notifier);
DCHECK_GE(stream_index, 0);
std::string group_id = stream.hls_group_id;
std::string name = stream.hls_name;
std::string hls_playlist_name = stream.hls_playlist_name;
std::string hls_iframe_playlist_name = stream.hls_iframe_playlist_name;
std::string playlist_name = stream.hls_playlist_name;
const std::string& group_id = stream.hls_group_id;
const std::string& iframe_playlist_name = stream.hls_iframe_playlist_name;
const std::vector<std::string>& characteristics = stream.hls_characteristics;
if (name.empty()) {
name = base::StringPrintf("stream_%d", stream_index);
}
if (hls_playlist_name.empty()) {
hls_playlist_name = base::StringPrintf("stream_%d.m3u8", stream_index);
if (playlist_name.empty()) {
playlist_name = base::StringPrintf("stream_%d.m3u8", stream_index);
}
const bool kIFramesOnly = true;
std::list<std::unique_ptr<MuxerListener>> listeners;
listeners.emplace_back(new HlsNotifyMuxerListener(
hls_playlist_name, !kIFramesOnly, name, group_id, notifier));
if (!hls_iframe_playlist_name.empty()) {
playlist_name, !kIFramesOnly, name, group_id, characteristics, notifier));
if (!iframe_playlist_name.empty()) {
listeners.emplace_back(new HlsNotifyMuxerListener(
hls_iframe_playlist_name, kIFramesOnly, name, group_id, notifier));
iframe_playlist_name, kIFramesOnly, name, group_id,
std::vector<std::string>(), notifier));
}
return listeners;
}

View File

@ -9,6 +9,7 @@
#include <memory>
#include <string>
#include <vector>
namespace shaka {
class MpdNotifier;
@ -44,6 +45,7 @@ class MuxerListenerFactory {
std::string hls_name;
std::string hls_playlist_name;
std::string hls_iframe_playlist_name;
std::vector<std::string> hls_characteristics;
};
/// Create a new muxer listener.

View File

@ -161,4 +161,7 @@ message MediaInfo {
optional string media_file_url = 17;
optional string init_segment_url = 18;
optional string segment_template_url = 19;
// HLS only. Defines CHARACTERISTICS attribute of the stream.
repeated string hls_characteristics = 20;
}

View File

@ -91,6 +91,7 @@ MuxerListenerFactory::StreamData ToMuxerListenerData(
data.hls_name = stream.hls_name;
data.hls_playlist_name = stream.hls_playlist_name;
data.hls_iframe_playlist_name = stream.hls_iframe_playlist_name;
data.hls_characteristics = stream.hls_characteristics;
return data;
};

View File

@ -106,6 +106,7 @@ struct StreamDescriptor {
/// Optional value which contains a user-specified language tag. If specified,
/// this value overrides any language metadata in the input stream.
std::string language;
/// Required for audio when outputting HLS. It defines the name of the output
/// stream, which is not necessarily the same as output. This is used as the
/// `NAME` attribute for EXT-X-MEDIA.
@ -119,6 +120,9 @@ struct StreamDescriptor {
/// Optional for HLS output. It defines the name of the I-Frames only playlist
/// for the stream. For Video only. Usually ends with `.m3u8`.
std::string hls_iframe_playlist_name;
/// Optional for HLS output. It defines the CHARACTERISTICS attribute of the
/// stream.
std::vector<std::string> hls_characteristics;
};
class SHAKA_EXPORT Packager {