Merge branch 'main' into sbd

This commit is contained in:
Cosmin Stejerean 2024-02-22 20:49:13 -08:00
commit e8657721cd
56 changed files with 743 additions and 24 deletions

View File

@ -12,5 +12,5 @@ DASH specific stream descriptor fields
Optional semicolon separated list of values for DASH Role element. The Optional semicolon separated list of values for DASH Role element. The
value should be one of: **caption**, **subtitle**, **main**, **alternate**, value should be one of: **caption**, **subtitle**, **main**, **alternate**,
**supplementary**, **commentary**, **description** and **dub**. See **supplementary**, **commentary**, **description**, **dub** and **forced-subtitle** .
DASH (ISO/IEC 23009-1) specification for details. See DASH (ISO/IEC 23009-1) specification for details.

View File

@ -76,6 +76,14 @@ HLS options
The EXT-X-MEDIA-SEQUENCE documentation can be read here: The EXT-X-MEDIA-SEQUENCE documentation can be read here:
https://tools.ietf.org/html/rfc8216#section-4.3.3.2. https://tools.ietf.org/html/rfc8216#section-4.3.3.2.
--hls_start_time_offset <seconds>
Sets EXT-X-START on the media playlists to specify the preferred point
at wich the player should start playing.
A positive number indicates a time offset from the beginning of the playlist.
A negative number indicates a negative time offset from the end of the
last media segment in the playlist.
--hls_only=0|1 --hls_only=0|1
Optional. Defaults to 0 if not specified. If it is set to 1, indicates the Optional. Defaults to 0 if not specified. If it is set to 1, indicates the

View File

@ -73,6 +73,15 @@ These are the available fields:
CEA allows specifying up to 4 streams within a single video stream. If not CEA allows specifying up to 4 streams within a single video stream. If not
specified, all subtitles will be merged together. specified, all subtitles will be merged together.
:forced_subtitle:
Optional boolean value (0|1). If set to 1 indicates that this stream is a
Forced Narrative subtitle that should be displayed when subtitles are otherwise
off, for example used to caption short portions of the audio that might be in
a foreign language. For DASH this will set role to **forced_subtitle**, for HLS
it will set FORCED=YES and AUTOSELECT=YES. Only valid for subtitles.
.. include:: /options/drm_stream_descriptors.rst .. include:: /options/drm_stream_descriptors.rst
.. include:: /options/dash_stream_descriptors.rst .. include:: /options/dash_stream_descriptors.rst
.. include:: /options/hls_stream_descriptors.rst .. include:: /options/hls_stream_descriptors.rst

View File

@ -8,6 +8,7 @@
#define PACKAGER_PUBLIC_HLS_PARAMS_H_ #define PACKAGER_PUBLIC_HLS_PARAMS_H_
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
namespace shaka { namespace shaka {
@ -63,6 +64,12 @@ struct HlsParams {
/// Custom EXT-X-MEDIA-SEQUENCE value to allow continuous media playback /// Custom EXT-X-MEDIA-SEQUENCE value to allow continuous media playback
/// across packager restarts. See #691 for details. /// across packager restarts. See #691 for details.
uint32_t media_sequence_number = 0; uint32_t media_sequence_number = 0;
/// Sets EXT-X-START on the media playlists to specify the preferred point
/// at wich the player should start playing.
/// A positive number indicates a time offset from the beginning of the
/// playlist. A negative number indicates a negative time offset from the end
/// of the last media segment in the playlist.
std::optional<double> start_time_offset;
}; };
} // namespace shaka } // namespace shaka

View File

@ -152,6 +152,9 @@ struct StreamDescriptor {
/// Set to true to indicate that the stream is for hls only. /// Set to true to indicate that the stream is for hls only.
bool hls_only = false; bool hls_only = false;
/// Optional, indicates if this is a Forced Narrative subtitle stream.
bool forced_subtitle = false;
/// Optional for DASH output. It defines the Label element in Adaptation Set. /// Optional for DASH output. It defines the Label element in Adaptation Set.
std::string dash_label; std::string dash_label;
}; };

View File

@ -6,6 +6,8 @@
#include <packager/app/hls_flags.h> #include <packager/app/hls_flags.h>
#include <optional>
ABSL_FLAG(std::string, ABSL_FLAG(std::string,
hls_master_playlist_output, hls_master_playlist_output,
"", "",
@ -35,3 +37,12 @@ ABSL_FLAG(int32_t,
"EXT-X-MEDIA-SEQUENCE value, which allows continuous media " "EXT-X-MEDIA-SEQUENCE value, which allows continuous media "
"sequence across packager restarts. See #691 for more " "sequence across packager restarts. See #691 for more "
"information about the reasoning of this and its use cases."); "information about the reasoning of this and its use cases.");
ABSL_FLAG(std::optional<double>,
hls_start_time_offset,
std::nullopt,
"Floating-point number. Sets EXT-X-START on the media playlists "
"to specify the preferred point at wich the player should start "
"playing. A positive number indicates a time offset from the "
"beginning of the playlist. A negative number indicates a "
"negative time offset from the end of the last media segment "
"in the playlist.");

View File

@ -15,5 +15,6 @@ ABSL_DECLARE_FLAG(std::string, hls_base_url);
ABSL_DECLARE_FLAG(std::string, hls_key_uri); ABSL_DECLARE_FLAG(std::string, hls_key_uri);
ABSL_DECLARE_FLAG(std::string, hls_playlist_type); ABSL_DECLARE_FLAG(std::string, hls_playlist_type);
ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number); ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number);
ABSL_DECLARE_FLAG(std::optional<double>, hls_start_time_offset);
#endif // PACKAGER_APP_HLS_FLAGS_H_ #endif // PACKAGER_APP_HLS_FLAGS_H_

View File

@ -120,8 +120,16 @@ const char kUsage[] =
" in the format: scheme_id_uri=value.\n" " in the format: scheme_id_uri=value.\n"
" - dash_roles (roles): Optional semicolon separated list of values for\n" " - dash_roles (roles): Optional semicolon separated list of values for\n"
" DASH Role elements. The value should be one of: caption, subtitle,\n" " DASH Role elements. The value should be one of: caption, subtitle,\n"
" main, alternate, supplementary, commentary, description and dub. See\n" " forced-subtitle, main, alternate, supplementary, commentary, \n"
" DASH (ISO/IEC 23009-1) specification for details.\n"; " description and dub. See DASH\n"
" (ISO/IEC 23009-1) specification for details.\n"
" - forced_subtitle: Optional boolean value (0|1). If set to 1 \n"
" indicates that this stream is a Forced Narrative subtitle that \n"
" should be displayed when subtitles are otherwise off, for example \n"
" used to caption short portions of the audio that might be in a \n"
" foreign language. For DASH this will set role to forced_subtitle, \n"
" for HLS it will set FORCED=YES and AUTOSELECT=YES. \n"
" Only valid for subtitles.";
// Labels for parameters in RawKey key info. // Labels for parameters in RawKey key info.
const char kDrmLabelLabel[] = "label"; const char kDrmLabelLabel[] = "label";
@ -587,6 +595,7 @@ std::optional<PackagingParams> GetPackagingParams() {
hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language); hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language);
hls_params.media_sequence_number = hls_params.media_sequence_number =
absl::GetFlag(FLAGS_hls_media_sequence_number); absl::GetFlag(FLAGS_hls_media_sequence_number);
hls_params.start_time_offset = absl::GetFlag(FLAGS_hls_start_time_offset);
TestParams& test_params = packaging_params.test_params; TestParams& test_params = packaging_params.test_params;
test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info); test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info);

View File

@ -40,6 +40,7 @@ enum FieldType {
kDashOnlyField, kDashOnlyField,
kHlsOnlyField, kHlsOnlyField,
kDashLabelField, kDashLabelField,
kForcedSubtitleField,
}; };
struct FieldNameToTypeMapping { struct FieldNameToTypeMapping {
@ -88,6 +89,7 @@ const FieldNameToTypeMapping kFieldNameTypeMappings[] = {
{"dash_only", kDashOnlyField}, {"dash_only", kDashOnlyField},
{"hls_only", kHlsOnlyField}, {"hls_only", kHlsOnlyField},
{"dash_label", kDashLabelField}, {"dash_label", kDashLabelField},
{"forced_subtitle", kForcedSubtitleField},
}; };
FieldType GetFieldType(const std::string& field_name) { FieldType GetFieldType(const std::string& field_name) {
@ -255,12 +257,35 @@ std::optional<StreamDescriptor> ParseStreamDescriptor(
case kDashLabelField: case kDashLabelField:
descriptor.dash_label = pair.second; descriptor.dash_label = pair.second;
break; break;
case kForcedSubtitleField:
unsigned forced_subtitle_value;
if (!absl::SimpleAtoi(pair.second, &forced_subtitle_value)) {
LOG(ERROR) << "Non-numeric option for forced field "
"specified ("
<< pair.second << ").";
return std::nullopt;
}
if (forced_subtitle_value > 1) {
LOG(ERROR) << "forced should be either 0 or 1.";
return std::nullopt;
}
descriptor.forced_subtitle = forced_subtitle_value > 0;
break;
default: default:
LOG(ERROR) << "Unknown field in stream descriptor (\"" << pair.first LOG(ERROR) << "Unknown field in stream descriptor (\"" << pair.first
<< "\")."; << "\").";
return std::nullopt; return std::nullopt;
} }
} }
if (descriptor.forced_subtitle) {
auto itr = std::find(descriptor.dash_roles.begin(),
descriptor.dash_roles.end(), "forced-subtitle");
if (itr == descriptor.dash_roles.end()) {
descriptor.dash_roles.push_back("forced-subtitle");
}
}
return descriptor; return descriptor;
} }

View File

@ -310,7 +310,8 @@ class PackagerAppTest(unittest.TestCase):
skip_encryption=None, skip_encryption=None,
bandwidth=None, bandwidth=None,
split_content_on_ad_cues=False, split_content_on_ad_cues=False,
test_file=None): test_file=None,
forced_subtitle=None):
"""Get a stream descriptor as a string. """Get a stream descriptor as a string.
@ -347,8 +348,9 @@ class PackagerAppTest(unittest.TestCase):
into multiple files, with a total of NumAdCues + 1 files. into multiple files, with a total of NumAdCues + 1 files.
test_file: The input file to use. If the input file is not specified, a test_file: The input file to use. If the input file is not specified, a
default file will be used. default file will be used.
forced_subtitle: If set to true, it marks this as a Forced Narrative
subtitle, marked in DASH using forced-subtitle role and
in HLS using FORCED=YES.
Returns: Returns:
A string that makes up a single stream descriptor for input to the A string that makes up a single stream descriptor for input to the
packager. packager.
@ -402,6 +404,9 @@ class PackagerAppTest(unittest.TestCase):
if dash_only: if dash_only:
stream.Append('dash_only', 1) stream.Append('dash_only', 1)
if forced_subtitle:
stream.Append('forced_subtitle', 1)
if dash_label: if dash_label:
stream.Append('dash_label', dash_label) stream.Append('dash_label', dash_label)
@ -830,7 +835,6 @@ class PackagerFunctionalTest(PackagerAppTest):
self._GetFlags(output_dash=True, output_hls=True)) self._GetFlags(output_dash=True, output_hls=True))
self._CheckTestResults('hls-only-dash-only') self._CheckTestResults('hls-only-dash-only')
def testDashLabel(self): def testDashLabel(self):
streams = [ streams = [
self._GetStream('video', dash_label='Main'), self._GetStream('video', dash_label='Main'),
@ -858,6 +862,21 @@ class PackagerFunctionalTest(PackagerAppTest):
sbd_key_audio='k5=v5:k6=v6')) sbd_key_audio='k5=v5:k6=v6'))
self._CheckTestResults('sbd_test') self._CheckTestResults('sbd_test')
def testForcedSubtitle(self):
streams = [
self._GetStream('audio', hls=True),
self._GetStream('video', hls=True),
]
streams += self._GetStreams(
['text'],
test_files=['bear-english.vtt'],
forced_subtitle=True)
self.assertPackageSuccess(streams, self._GetFlags(output_dash=True,
output_hls=True))
self._CheckTestResults('forced-subtitle')
def testAudioVideoWithLanguageOverride(self): def testAudioVideoWithLanguageOverride(self):
self.assertPackageSuccess( self.assertPackageSuccess(
self._GetStreams(['audio', 'video'], language='por', hls=True), self._GetStreams(['audio', 'video'], language='por', hls=True),
@ -966,6 +985,13 @@ class PackagerFunctionalTest(PackagerAppTest):
self._GetFlags(output_dash=True)) self._GetFlags(output_dash=True))
self._CheckTestResults('acc-he') self._CheckTestResults('acc-he')
def testDtsx(self):
self.assertPackageSuccess(
self._GetStreams(
['audio'], test_files=['bear-dtsx.mp4']),
self._GetFlags(output_dash=True))
self._CheckTestResults('dtsx-dash')
def testVideoAudioWebVTT(self): def testVideoAudioWebVTT(self):
audio_video_streams = self._GetStreams(['audio', 'video']) audio_video_streams = self._GetStreams(['audio', 'video'])
text_stream = self._GetStreams(['text'], test_files=['bear-english.vtt']) text_stream = self._GetStreams(['text'], test_files=['bear-english.vtt'])

Binary file not shown.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT3.114667S">
<Period id="0">
<AdaptationSet id="0" contentType="audio" subsegmentAlignment="true">
<Representation id="0" bandwidth="227665" codecs="dtsx" mimeType="audio/mp4" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="tag:dts.com,2018:uhd:audio_channel_configuration" value="0000003F"/>
<BaseURL>bear-dtsx-audio.mp4</BaseURL>
<SegmentBase indexRange="742-821" timescale="48000">
<Initialization range="0-741"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="bear-640x360-audio.mp4",BYTERANGE="804@0"
#EXTINF:1.022,
#EXT-X-BYTERANGE:17028@872
bear-640x360-audio.mp4
#EXTINF:0.998,
#EXT-X-BYTERANGE:16285
bear-640x360-audio.mp4
#EXTINF:0.720,
#EXT-X-BYTERANGE:9558
bear-640x360-audio.mp4
#EXT-X-ENDLIST

Binary file not shown.

View File

@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-I-FRAMES-ONLY
#EXT-X-MAP:URI="bear-640x360-video.mp4",BYTERANGE="870@0"
#EXTINF:1.001,
#EXT-X-BYTERANGE:15581@938
bear-640x360-video.mp4
#EXTINF:1.001,
#EXT-X-BYTERANGE:18221@100251
bear-640x360-video.mp4
#EXTINF:0.734,
#EXT-X-BYTERANGE:19663@222058
bear-640x360-video.mp4
#EXT-X-ENDLIST

View File

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="bear-640x360-video.mp4",BYTERANGE="870@0"
#EXTINF:1.001,
#EXT-X-BYTERANGE:99313@938
bear-640x360-video.mp4
#EXTINF:1.001,
#EXT-X-BYTERANGE:121807
bear-640x360-video.mp4
#EXTINF:0.734,
#EXT-X-BYTERANGE:79662
bear-640x360-video.mp4
#EXT-X-ENDLIST

Binary file not shown.

View File

@ -0,0 +1,11 @@
WEBVTT
STYLE
::cue { color:lime }
00:00:00.000 --> 00:00:00.800 align:center
Yup, that's a bear, eh.
00:00:01.000 --> 00:00:04.700 align:center
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,13 @@
#EXTM3U
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_2.m3u8",GROUP-ID="default-text-group",NAME="stream_2",DEFAULT=NO,AUTOSELECT=YES,FORCED=YES
#EXT-X-STREAM-INF:BANDWIDTH=1106817,AVERAGE-BANDWIDTH=1004632,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group",CLOSED-CAPTIONS=NONE
bear-640x360-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=214292,AVERAGE-BANDWIDTH=156327,CODECS="avc1.64001e",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,URI="bear-640x360-video-iframe.m3u8"

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT2.736067S">
<Period id="0">
<AdaptationSet id="0" contentType="audio" subsegmentAlignment="true">
<Representation id="0" bandwidth="133334" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>bear-640x360-audio.mp4</BaseURL>
<SegmentBase indexRange="804-871" timescale="44100">
<Initialization range="0-803"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" frameRate="30000/1001" subsegmentAlignment="true" par="16:9">
<Representation id="1" bandwidth="973483" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>bear-640x360-video.mp4</BaseURL>
<SegmentBase indexRange="870-937" timescale="30000">
<Initialization range="0-869"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="text" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="forced-subtitle"/>
<Representation id="2" bandwidth="317" mimeType="text/vtt">
<BaseURL>bear-english-text.vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,8 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:4.700,
bear-english-text.vtt
#EXT-X-ENDLIST

View File

@ -318,11 +318,16 @@ void BuildMediaTag(const MediaPlaylist& playlist,
} else { } else {
tag.AddString("DEFAULT", "NO"); tag.AddString("DEFAULT", "NO");
} }
if (is_autoselect) { if (is_autoselect) {
tag.AddString("AUTOSELECT", "YES"); tag.AddString("AUTOSELECT", "YES");
} }
if (playlist.stream_type() ==
MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
playlist.forced_subtitle()) {
tag.AddString("FORCED", "YES");
}
const std::vector<std::string>& characteristics = playlist.characteristics(); const std::vector<std::string>& characteristics = playlist.characteristics();
if (!characteristics.empty()) { if (!characteristics.empty()) {
tag.AddQuotedString("CHARACTERISTICS", absl::StrJoin(characteristics, ",")); tag.AddQuotedString("CHARACTERISTICS", absl::StrJoin(characteristics, ","));
@ -401,6 +406,12 @@ void BuildMediaTags(
} }
} }
if (playlist->stream_type() ==
MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
playlist->forced_subtitle()) {
is_autoselect = true;
}
BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url, BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url,
out); out);
} }

View File

@ -10,6 +10,7 @@
#include <cinttypes> #include <cinttypes>
#include <cmath> #include <cmath>
#include <memory> #include <memory>
#include <optional>
#include <absl/log/check.h> #include <absl/log/check.h>
#include <absl/log/log.h> #include <absl/log/log.h>
@ -109,7 +110,8 @@ std::string CreatePlaylistHeader(
HlsPlaylistType type, HlsPlaylistType type,
MediaPlaylist::MediaPlaylistStreamType stream_type, MediaPlaylist::MediaPlaylistStreamType stream_type,
uint32_t media_sequence_number, uint32_t media_sequence_number,
int discontinuity_sequence_number) { int discontinuity_sequence_number,
std::optional<double> start_time_offset) {
const std::string version = GetPackagerVersion(); const std::string version = GetPackagerVersion();
std::string version_line; std::string version_line;
if (!version.empty()) { if (!version.empty()) {
@ -151,6 +153,10 @@ std::string CreatePlaylistHeader(
MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) { MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
absl::StrAppendFormat(&header, "#EXT-X-I-FRAMES-ONLY\n"); absl::StrAppendFormat(&header, "#EXT-X-I-FRAMES-ONLY\n");
} }
if (start_time_offset.has_value()) {
absl::StrAppendFormat(&header, "#EXT-X-START:TIME-OFFSET=%f\n",
start_time_offset.value());
}
// Put EXT-X-MAP at the end since the rest of the playlist is about the // Put EXT-X-MAP at the end since the rest of the playlist is about the
// segment and key info. // segment and key info.
@ -373,6 +379,10 @@ void MediaPlaylist::SetCharacteristicsForTesting(
characteristics_ = characteristics; characteristics_ = characteristics;
} }
void MediaPlaylist::SetForcedSubtitleForTesting(const bool forced_subtitle) {
forced_subtitle_ = forced_subtitle;
}
bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) { bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
const int32_t time_scale = GetTimeScale(media_info); const int32_t time_scale = GetTimeScale(media_info);
if (time_scale == 0) { if (time_scale == 0) {
@ -400,6 +410,8 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
std::vector<std::string>(media_info_.hls_characteristics().begin(), std::vector<std::string>(media_info_.hls_characteristics().begin(),
media_info_.hls_characteristics().end()); media_info_.hls_characteristics().end());
forced_subtitle_ = media_info_.forced_subtitle();
return true; return true;
} }
@ -479,7 +491,8 @@ bool MediaPlaylist::WriteToFile(const std::filesystem::path& file_path) {
std::string content = CreatePlaylistHeader( std::string content = CreatePlaylistHeader(
media_info_, target_duration_, hls_params_.playlist_type, stream_type_, media_info_, target_duration_, hls_params_.playlist_type, stream_type_,
media_sequence_number_, discontinuity_sequence_number_); media_sequence_number_, discontinuity_sequence_number_,
hls_params_.start_time_offset);
for (const auto& entry : entries_) for (const auto& entry : entries_)
absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str()); absl::StrAppendFormat(&content, "%s\n", entry->ToString().c_str());
@ -513,8 +526,8 @@ void MediaPlaylist::SetTargetDuration(int32_t target_duration) {
if (target_duration_set_) { if (target_duration_set_) {
if (target_duration_ == target_duration) if (target_duration_ == target_duration)
return; return;
VLOG(1) << "Updating target duration from " << target_duration << " to " VLOG(1) << "Updating target duration from " << target_duration_ << " to "
<< target_duration_; << target_duration;
} }
target_duration_ = target_duration; target_duration_ = target_duration;
target_duration_set_ = true; target_duration_set_ = true;

View File

@ -90,6 +90,9 @@ class MediaPlaylist {
/// For testing only. /// For testing only.
void SetLanguageForTesting(const std::string& language); void SetLanguageForTesting(const std::string& language);
/// For testing only.
void SetForcedSubtitleForTesting(const bool forced_subtitle);
/// For testing only. /// For testing only.
void SetCharacteristicsForTesting( void SetCharacteristicsForTesting(
const std::vector<std::string>& characteristics); const std::vector<std::string>& characteristics);
@ -223,6 +226,8 @@ class MediaPlaylist {
return characteristics_; return characteristics_;
} }
bool forced_subtitle() const { return forced_subtitle_; }
bool is_dvs() const { bool is_dvs() const {
// HLS Authoring Specification for Apple Devices // HLS Authoring Specification for Apple Devices
// https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices#overview // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices#overview
@ -262,6 +267,7 @@ class MediaPlaylist {
std::string codec_; std::string codec_;
std::string language_; std::string language_;
std::vector<std::string> characteristics_; std::vector<std::string> characteristics_;
bool forced_subtitle_ = false;
uint32_t media_sequence_number_ = 0; uint32_t media_sequence_number_ = 0;
bool inserted_discontinuity_tag_ = false; bool inserted_discontinuity_tag_ = false;
int discontinuity_sequence_number_ = 0; int discontinuity_sequence_number_ = 0;

View File

@ -51,6 +51,10 @@ class MediaPlaylistTest : public ::testing::Test {
default_group_id_("default_group_id") { default_group_id_("default_group_id") {
hls_params_.playlist_type = type; hls_params_.playlist_type = type;
hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth; hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth;
// NOTE: hls_params_ is passed by and stored by reference in MediaPlaylist,
// so changed made to it through mutable_hls_params() after this point
// still affect what the playlist see in its own hls_params_ later.
media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_, media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_,
default_name_, default_group_id_)); default_name_, default_group_id_));
} }
@ -658,6 +662,90 @@ TEST_F(MediaPlaylistMultiSegmentTest, MultipleEncryptionInfo) {
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
} }
TEST_F(MediaPlaylistSingleSegmentTest, StartTimeEmpty) {
const std::string kExpectedOutput =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/shaka-project/shaka-packager "
"version test\n"
"#EXT-X-TARGETDURATION:0\n"
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-ENDLIST\n";
// Because this is std::nullopt, the tag isn't in the playlist at all.
mutable_hls_params()->start_time_offset = std::nullopt;
ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistSingleSegmentTest, StartTimeZero) {
const std::string kExpectedOutput =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/shaka-project/shaka-packager "
"version test\n"
"#EXT-X-TARGETDURATION:0\n"
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-START:TIME-OFFSET=0.000000\n"
"#EXT-X-ENDLIST\n";
mutable_hls_params()->start_time_offset = 0;
ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistSingleSegmentTest, StartTimePositive) {
const std::string kExpectedOutput =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/shaka-project/shaka-packager "
"version test\n"
"#EXT-X-TARGETDURATION:0\n"
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-START:TIME-OFFSET=20.000000\n"
"#EXT-X-ENDLIST\n";
mutable_hls_params()->start_time_offset = 20;
ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
TEST_F(MediaPlaylistSingleSegmentTest, StartTimeNegative) {
const std::string kExpectedOutput =
"#EXTM3U\n"
"#EXT-X-VERSION:6\n"
"## Generated with https://github.com/shaka-project/shaka-packager "
"version test\n"
"#EXT-X-TARGETDURATION:0\n"
"#EXT-X-PLAYLIST-TYPE:VOD\n"
"#EXT-X-START:TIME-OFFSET=-3.141590\n"
"#EXT-X-ENDLIST\n";
mutable_hls_params()->start_time_offset = -3.14159;
ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_));
const char kMemoryFilePath[] = "memory://media.m3u8";
EXPECT_TRUE(media_playlist_->WriteToFile(kMemoryFilePath));
ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput);
}
class LiveMediaPlaylistTest : public MediaPlaylistMultiSegmentTest { class LiveMediaPlaylistTest : public MediaPlaylistMultiSegmentTest {
protected: protected:
LiveMediaPlaylistTest() LiveMediaPlaylistTest()

View File

@ -150,6 +150,8 @@ std::string AudioStreamInfo::GetCodecString(Codec codec,
return "dts-"; return "dts-";
case kCodecDTSP: case kCodecDTSP:
return "dts+"; return "dts+";
case kCodecDTSX:
return "dtsx";
case kCodecEAC3: case kCodecEAC3:
return "ec-3"; return "ec-3";
case kCodecAC4: case kCodecAC4:

View File

@ -58,6 +58,7 @@ enum FourCC : uint32_t {
FOURCC_dtsl = 0x6474736c, FOURCC_dtsl = 0x6474736c,
FOURCC_dtsm = 0x6474732d, // "dts-" FOURCC_dtsm = 0x6474732d, // "dts-"
FOURCC_dtsp = 0x6474732b, // "dts+" FOURCC_dtsp = 0x6474732b, // "dts+"
FOURCC_dtsx = 0x64747378, // "dtsx"
FOURCC_dvcC = 0x64766343, FOURCC_dvcC = 0x64766343,
FOURCC_dvh1 = 0x64766831, FOURCC_dvh1 = 0x64766831,
FOURCC_dvhe = 0x64766865, FOURCC_dvhe = 0x64766865,
@ -151,6 +152,7 @@ enum FourCC : uint32_t {
FOURCC_trex = 0x74726578, FOURCC_trex = 0x74726578,
FOURCC_trun = 0x7472756e, FOURCC_trun = 0x7472756e,
FOURCC_udta = 0x75647461, FOURCC_udta = 0x75647461,
FOURCC_udts = 0x75647473, // "udts"
FOURCC_url = 0x75726c20, // "url " FOURCC_url = 0x75726c20, // "url "
FOURCC_urn = 0x75726e20, // "urn " FOURCC_urn = 0x75726e20, // "urn "
FOURCC_uuid = 0x75756964, FOURCC_uuid = 0x75756964,

View File

@ -49,6 +49,7 @@ enum Codec {
kCodecDTSL, kCodecDTSL,
kCodecDTSM, kCodecDTSM,
kCodecDTSP, kCodecDTSP,
kCodecDTSX,
kCodecEAC3, kCodecEAC3,
kCodecFlac, kCodecFlac,
kCodecOpus, kCodecOpus,

View File

@ -12,6 +12,7 @@ add_library(media_codecs STATIC
avc_decoder_configuration_record.cc avc_decoder_configuration_record.cc
decoder_configuration_record.cc decoder_configuration_record.cc
dovi_decoder_configuration_record.cc dovi_decoder_configuration_record.cc
dts_audio_specific_config.cc
ec3_audio_util.cc ec3_audio_util.cc
ac4_audio_util.cc ac4_audio_util.cc
es_descriptor.cc es_descriptor.cc

View File

@ -0,0 +1,28 @@
// Copyright (c) 2023 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.
#include <packager/media/codecs/dts_audio_specific_config.h>
#include <packager/media/base/bit_reader.h>
#include <packager/media/base/rcheck.h>
namespace shaka {
namespace media {
bool GetDTSXChannelMask(const std::vector<uint8_t>& udts, uint32_t& mask) {
// udts is the DTS-UHD Specific Box: ETSI TS 103 491 V1.2.1 Table B-2
// DecoderProfileCode(6 bits)
// FrameDurationCode(2 bits)
// MaxPayloadCode(3 bits)
// NumPresentationsCode(5 bits)
// ChannelMask (32 bits)
BitReader bit_reader(udts.data(), udts.size());
RCHECK(bit_reader.SkipBits(16));
RCHECK(bit_reader.ReadBits(32, &mask));
return true;
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,24 @@
// Copyright (c) 2023 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.
#ifndef PACKAGER_MEDIA_CODECS_DTS_AUDIO_SPECIFIC_CONFIG_H_
#define PACKAGER_MEDIA_CODECS_DTS_AUDIO_SPECIFIC_CONFIG_H_
#include <stddef.h>
#include <stdint.h>
#include <vector>
namespace shaka {
namespace media {
class BitReader;
bool GetDTSXChannelMask(const std::vector<uint8_t>& udts, uint32_t& mask);
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_CODECS_DTS_AUDIO_SPECIFIC_CONFIG_H_

View File

@ -0,0 +1,37 @@
// Copyright 2023 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.
#include <gtest/gtest.h>
#include "packager/media/codecs/dts_audio_specific_config.h"
namespace shaka {
namespace media {
TEST(DTSAudioSpecificConfigTest, BasicProfileTest) {
uint8_t buffer[] = {0x01, 0x20, 0x00, 0x00, 0x0, 0x3F, 0x80, 0x00};
std::vector<uint8_t> data(std::begin(buffer), std::end(buffer));
uint32_t mask;
EXPECT_TRUE(GetDTSXChannelMask(data, mask));
EXPECT_EQ(0x3F, mask);
}
TEST(DTSAudioSpecificConfigTest, ChannelMaskBytes) {
uint8_t buffer[] = {0x01, 0x20, 0x12, 0x34, 0x56, 0x78, 0x80, 0x00};
std::vector<uint8_t> data(std::begin(buffer), std::end(buffer));
uint32_t mask;
EXPECT_TRUE(GetDTSXChannelMask(data, mask));
EXPECT_EQ(0x12345678, mask);
}
TEST(DTSAudioSpecificConfigTest, Truncated) {
uint8_t buffer[] = {0x01, 0x20, 0x00, 0x00, 0x00};
std::vector<uint8_t> data(std::begin(buffer), std::end(buffer));
uint32_t mask;
EXPECT_FALSE(GetDTSXChannelMask(data, mask));
}
} // namespace media
} // namespace shaka

View File

@ -74,21 +74,24 @@ bool H265ByteToUnitStreamConverter::GetDecoderConfigurationRecord(
buffer.AppendInt(static_cast<uint8_t>(kUnitStreamNaluLengthSize - 1)); buffer.AppendInt(static_cast<uint8_t>(kUnitStreamNaluLengthSize - 1));
buffer.AppendInt(static_cast<uint8_t>(3) /* numOfArrays */); buffer.AppendInt(static_cast<uint8_t>(3) /* numOfArrays */);
// More parameter set NALUs may follow when strip_parameter_set_nalus is
// disabled.
const uint8_t array_completeness = strip_parameter_set_nalus() ? 0x80 : 0;
// VPS // VPS
const uint8_t kArrayCompleteness = 0x80; buffer.AppendInt(static_cast<uint8_t>(array_completeness | Nalu::H265_VPS));
buffer.AppendInt(static_cast<uint8_t>(kArrayCompleteness | Nalu::H265_VPS));
buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */); buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */);
buffer.AppendInt(static_cast<uint16_t>(last_vps_.size())); buffer.AppendInt(static_cast<uint16_t>(last_vps_.size()));
buffer.AppendVector(last_vps_); buffer.AppendVector(last_vps_);
// SPS // SPS
buffer.AppendInt(static_cast<uint8_t>(kArrayCompleteness | Nalu::H265_SPS)); buffer.AppendInt(static_cast<uint8_t>(array_completeness | Nalu::H265_SPS));
buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */); buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */);
buffer.AppendInt(static_cast<uint16_t>(last_sps_.size())); buffer.AppendInt(static_cast<uint16_t>(last_sps_.size()));
buffer.AppendVector(last_sps_); buffer.AppendVector(last_sps_);
// PPS // PPS
buffer.AppendInt(static_cast<uint8_t>(kArrayCompleteness | Nalu::H265_PPS)); buffer.AppendInt(static_cast<uint8_t>(array_completeness | Nalu::H265_PPS));
buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */); buffer.AppendInt(static_cast<uint16_t>(1) /* numNalus */);
buffer.AppendInt(static_cast<uint16_t>(last_pps_.size())); buffer.AppendInt(static_cast<uint16_t>(last_pps_.size()));
buffer.AppendVector(last_pps_); buffer.AppendVector(last_pps_);

View File

@ -26,6 +26,7 @@ HlsNotifyMuxerListener::HlsNotifyMuxerListener(
const std::string& ext_x_media_name, const std::string& ext_x_media_name,
const std::string& ext_x_media_group_id, const std::string& ext_x_media_group_id,
const std::vector<std::string>& characteristics, const std::vector<std::string>& characteristics,
bool forced_subtitle,
hls::HlsNotifier* hls_notifier, hls::HlsNotifier* hls_notifier,
std::optional<uint32_t> index) std::optional<uint32_t> index)
: playlist_name_(playlist_name), : playlist_name_(playlist_name),
@ -33,6 +34,7 @@ HlsNotifyMuxerListener::HlsNotifyMuxerListener(
ext_x_media_name_(ext_x_media_name), ext_x_media_name_(ext_x_media_name),
ext_x_media_group_id_(ext_x_media_group_id), ext_x_media_group_id_(ext_x_media_group_id),
characteristics_(characteristics), characteristics_(characteristics),
forced_subtitle_(forced_subtitle),
hls_notifier_(hls_notifier), hls_notifier_(hls_notifier),
index_(index) { index_(index) {
DCHECK(hls_notifier); DCHECK(hls_notifier);
@ -103,6 +105,9 @@ void HlsNotifyMuxerListener::OnMediaStart(const MuxerOptions& muxer_options,
for (const std::string& characteristic : characteristics_) for (const std::string& characteristic : characteristics_)
media_info->add_hls_characteristics(characteristic); media_info->add_hls_characteristics(characteristic);
} }
if (forced_subtitle_) {
media_info->set_forced_subtitle(forced_subtitle_);
}
if (index_.has_value()) if (index_.has_value())
media_info->set_index(index_.value()); media_info->set_index(index_.value());

View File

@ -39,12 +39,16 @@ class HlsNotifyMuxerListener : public MuxerListener {
/// @param characteristics is the characteristics for this playlist. This is /// @param characteristics is the characteristics for this playlist. This is
/// the value of CHARACTERISTICS attribute for EXT-X-MEDIA. This may be /// the value of CHARACTERISTICS attribute for EXT-X-MEDIA. This may be
/// empty. /// empty.
/// @param forced is the HLS FORCED SUBTITLE setting for this playlist. This
/// is the value of FORCED attribute for EXT-X-MEDIA. This may be
/// empty.
/// @param hls_notifier used by this listener. Ownership does not transfer. /// @param hls_notifier used by this listener. Ownership does not transfer.
HlsNotifyMuxerListener(const std::string& playlist_name, HlsNotifyMuxerListener(const std::string& playlist_name,
bool iframes_only, bool iframes_only,
const std::string& ext_x_media_name, const std::string& ext_x_media_name,
const std::string& ext_x_media_group_id, const std::string& ext_x_media_group_id,
const std::vector<std::string>& characteristics, const std::vector<std::string>& characteristics,
bool forced,
hls::HlsNotifier* hls_notifier, hls::HlsNotifier* hls_notifier,
std::optional<uint32_t> index); std::optional<uint32_t> index);
~HlsNotifyMuxerListener() override; ~HlsNotifyMuxerListener() override;
@ -86,6 +90,7 @@ class HlsNotifyMuxerListener : public MuxerListener {
const std::string ext_x_media_name_; const std::string ext_x_media_name_;
const std::string ext_x_media_group_id_; const std::string ext_x_media_group_id_;
const std::vector<std::string> characteristics_; const std::vector<std::string> characteristics_;
const bool forced_subtitle_;
hls::HlsNotifier* const hls_notifier_; hls::HlsNotifier* const hls_notifier_;
std::optional<uint32_t> stream_id_; std::optional<uint32_t> stream_id_;
std::optional<uint32_t> index_; std::optional<uint32_t> index_;

View File

@ -99,6 +99,7 @@ const char kDefaultName[] = "DEFAULTNAME";
const char kDefaultGroupId[] = "DEFAULTGROUPID"; const char kDefaultGroupId[] = "DEFAULTGROUPID";
const char kCharactersticA[] = "public.accessibility.transcribes-spoken-dialog"; const char kCharactersticA[] = "public.accessibility.transcribes-spoken-dialog";
const char kCharactersticB[] = "public.easy-to-read"; const char kCharactersticB[] = "public.easy-to-read";
const bool kForced = false;
MATCHER_P(HasEncryptionScheme, expected_scheme, "") { MATCHER_P(HasEncryptionScheme, expected_scheme, "") {
*result_listener << "it has_protected_content: " *result_listener << "it has_protected_content: "
@ -121,6 +122,7 @@ class HlsNotifyMuxerListenerTest : public ::testing::Test {
kDefaultName, kDefaultName,
kDefaultGroupId, kDefaultGroupId,
std::vector<std::string>{kCharactersticA, kCharactersticB}, std::vector<std::string>{kCharactersticA, kCharactersticB},
kForced,
&mock_notifier_, &mock_notifier_,
0) {} 0) {}
@ -459,6 +461,7 @@ class HlsNotifyMuxerListenerKeyFrameTest : public TestWithParam<bool> {
kDefaultName, kDefaultName,
kDefaultGroupId, kDefaultGroupId,
std::vector<std::string>(), // no characteristics. std::vector<std::string>(), // no characteristics.
kForced,
&mock_notifier_, &mock_notifier_,
0) {} 0) {}

View File

@ -62,6 +62,7 @@ std::list<std::unique_ptr<MuxerListener>> CreateHlsListenersInternal(
const std::string& group_id = stream.hls_group_id; const std::string& group_id = stream.hls_group_id;
const std::string& iframe_playlist_name = stream.hls_iframe_playlist_name; const std::string& iframe_playlist_name = stream.hls_iframe_playlist_name;
const std::vector<std::string>& characteristics = stream.hls_characteristics; const std::vector<std::string>& characteristics = stream.hls_characteristics;
const bool forced_subtitle = stream.forced_subtitle;
if (name.empty()) { if (name.empty()) {
name = absl::StrFormat("stream_%d", stream_index); name = absl::StrFormat("stream_%d", stream_index);
@ -73,13 +74,13 @@ std::list<std::unique_ptr<MuxerListener>> CreateHlsListenersInternal(
const bool kIFramesOnly = true; const bool kIFramesOnly = true;
std::list<std::unique_ptr<MuxerListener>> listeners; std::list<std::unique_ptr<MuxerListener>> listeners;
listeners.emplace_back( listeners.emplace_back(new HlsNotifyMuxerListener(
new HlsNotifyMuxerListener(playlist_name, !kIFramesOnly, name, group_id, playlist_name, !kIFramesOnly, name, group_id, characteristics,
characteristics, notifier, stream.index)); forced_subtitle, notifier, stream.index));
if (!iframe_playlist_name.empty()) { if (!iframe_playlist_name.empty()) {
listeners.emplace_back(new HlsNotifyMuxerListener( listeners.emplace_back(new HlsNotifyMuxerListener(
iframe_playlist_name, kIFramesOnly, name, group_id, iframe_playlist_name, kIFramesOnly, name, group_id,
std::vector<std::string>(), notifier, stream.index)); std::vector<std::string>(), forced_subtitle, notifier, stream.index));
} }
return listeners; return listeners;
} }

View File

@ -47,6 +47,7 @@ class MuxerListenerFactory {
std::string hls_playlist_name; std::string hls_playlist_name;
std::string hls_iframe_playlist_name; std::string hls_iframe_playlist_name;
std::vector<std::string> hls_characteristics; std::vector<std::string> hls_characteristics;
bool forced_subtitle = false;
bool hls_only = false; bool hls_only = false;
// DASH specific values needed to write DASH mpd. Will only be used if an // DASH specific values needed to write DASH mpd. Will only be used if an

View File

@ -20,6 +20,7 @@
#include <packager/media/base/text_stream_info.h> #include <packager/media/base/text_stream_info.h>
#include <packager/media/base/video_stream_info.h> #include <packager/media/base/video_stream_info.h>
#include <packager/media/codecs/ac4_audio_util.h> #include <packager/media/codecs/ac4_audio_util.h>
#include <packager/media/codecs/dts_audio_specific_config.h>
#include <packager/media/codecs/ec3_audio_util.h> #include <packager/media/codecs/ec3_audio_util.h>
#include <packager/mpd/base/media_info.pb.h> #include <packager/mpd/base/media_info.pb.h>
#include <packager/utils/bytes_to_string_view.h> #include <packager/utils/bytes_to_string_view.h>
@ -165,6 +166,16 @@ void AddAudioInfo(const AudioStreamInfo* audio_stream_info,
codec_data->set_ac4_ims_flag(ac4_ims_flag); codec_data->set_ac4_ims_flag(ac4_ims_flag);
codec_data->set_ac4_cbi_flag(ac4_cbi_flag); codec_data->set_ac4_cbi_flag(ac4_cbi_flag);
} }
if (audio_stream_info->codec() == kCodecDTSX) {
auto* codec_data = audio_info->mutable_codec_specific_data();
uint32_t channel_mask;
if (!GetDTSXChannelMask(codec_config, channel_mask)) {
LOG(ERROR) << "Failed to parse DTSX channel mask.";
return;
}
codec_data->set_channel_mask(channel_mask);
}
} }
void AddTextInfo(const TextStreamInfo& text_stream_info, void AddTextInfo(const TextStreamInfo& text_stream_info,

View File

@ -72,6 +72,24 @@ TEST_F(MuxerListenerInternalVideoStreamTest, TransferCharacteristics) {
EXPECT_EQ(18u, media_info.video_info().transfer_characteristics()); EXPECT_EQ(18u, media_info.video_info().transfer_characteristics());
} }
class MuxerListenerInternalAudioStreamTest : public MuxerListenerInternalTest {
};
// AddAudioInfo function should parse the channel mask
TEST_F(MuxerListenerInternalAudioStreamTest, DTSX) {
MediaInfo media_info;
std::shared_ptr<AudioStreamInfo> audio_info = CreateAudioStreamInfo(
GetAudioStreamInfoParams(kCodecDTSX, "dtsx",
{0x01, 0x20, 0x00, 0x00, 0x0, 0x3F, 0x80,
0x00})); // Channel mask = 3F
ASSERT_TRUE(GenerateMediaInfo(MuxerOptions(), *audio_info,
kReferenceTimeScale,
MuxerListener::kContainerMp4, &media_info));
MediaInfo_AudioInfo* info = media_info.mutable_audio_info();
auto* codec_data = info->mutable_codec_specific_data();
EXPECT_EQ(0x3F, codec_data->channel_mask());
}
} // namespace internal } // namespace internal
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -104,5 +104,53 @@ std::vector<ProtectionSystemSpecificInfo> GetDefaultKeySystemInfo() {
std::end(kExpectedDefaultPsshBox) - 1}}}; std::end(kExpectedDefaultPsshBox) - 1}}};
} }
AudioStreamInfoParameters::AudioStreamInfoParameters() {}
AudioStreamInfoParameters::~AudioStreamInfoParameters() {}
std::shared_ptr<AudioStreamInfo> CreateAudioStreamInfo(
const AudioStreamInfoParameters& param) {
return std::make_shared<AudioStreamInfo>(
param.track_id, param.time_scale, param.duration, param.codec,
param.codec_string, param.codec_config.data(), param.codec_config.size(),
param.sample_bits, param.num_channels, param.sampling_frequency,
param.seek_preroll_ns, param.codec_delay_ns, param.max_bitrate,
param.avg_bitrate, param.language, param.is_encrypted);
}
AudioStreamInfoParameters GetAudioStreamInfoParams(
Codec codec,
const char* codec_string,
const std::vector<uint8_t>& codec_config) {
const int kTrackId = 0;
const int32_t kTimeScale = 10;
const int64_t kAudioStreamDuration = 200;
const char* kLanuageUndefined = "und";
const uint8_t kSampleBits = 16;
const uint8_t kNumChannels = 6;
const uint32_t kSamplingFrequency = 48000;
const uint64_t kSeekPrerollNs = 0;
const uint64_t kCodecDelayNs = 0;
const uint32_t kMaxBitrate = 0;
const uint32_t kAvgBitrate = 0;
const bool kEncryptedFlag = false;
AudioStreamInfoParameters params;
params.track_id = kTrackId;
params.time_scale = kTimeScale;
params.duration = kAudioStreamDuration;
params.codec = codec;
params.codec_string = codec_string;
params.language = kLanuageUndefined;
params.sample_bits = kSampleBits;
params.num_channels = kNumChannels;
params.sampling_frequency = kSamplingFrequency;
params.seek_preroll_ns = kSeekPrerollNs;
params.codec_delay_ns = kCodecDelayNs;
params.max_bitrate = kMaxBitrate;
params.avg_bitrate = kAvgBitrate;
params.codec_config = codec_config;
params.is_encrypted = kEncryptedFlag;
return params;
}
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -10,6 +10,7 @@
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#include <packager/media/base/audio_stream_info.h>
#include <packager/media/base/key_source.h> #include <packager/media/base/key_source.h>
#include <packager/media/base/muxer_options.h> #include <packager/media/base/muxer_options.h>
#include <packager/media/base/stream_info.h> #include <packager/media/base/stream_info.h>
@ -95,6 +96,29 @@ struct VideoStreamInfoParameters {
bool is_encrypted; bool is_encrypted;
}; };
// Struct that gets passed for to CreateAudioStreamInfo() to create a
// StreamInfo instance. Useful for generating multiple AudioStreamInfo with
// slightly different parameters.
struct AudioStreamInfoParameters {
AudioStreamInfoParameters();
~AudioStreamInfoParameters();
int track_id;
int32_t time_scale;
int64_t duration;
Codec codec;
std::string codec_string;
std::vector<uint8_t> codec_config;
uint8_t sample_bits;
uint8_t num_channels;
uint32_t sampling_frequency;
uint64_t seek_preroll_ns;
uint64_t codec_delay_ns;
uint32_t max_bitrate;
uint32_t avg_bitrate;
std::string language;
bool is_encrypted;
};
struct OnNewSegmentParameters { struct OnNewSegmentParameters {
std::string file_name; std::string file_name;
int64_t start_time; int64_t start_time;
@ -115,6 +139,16 @@ std::shared_ptr<VideoStreamInfo> CreateVideoStreamInfo(
// Returns the "default" VideoStreamInfoParameters for testing. // Returns the "default" VideoStreamInfoParameters for testing.
VideoStreamInfoParameters GetDefaultVideoStreamInfoParams(); VideoStreamInfoParameters GetDefaultVideoStreamInfoParams();
// Creates StreamInfo instance from AudioStreamInfoParameters.
std::shared_ptr<AudioStreamInfo> CreateAudioStreamInfo(
const AudioStreamInfoParameters& param);
// Returns the "default" configuration for testing given codec and parameters.
AudioStreamInfoParameters GetAudioStreamInfoParams(
Codec codec,
const char* codec_string,
const std::vector<uint8_t>& codec_config);
// Returns the "default" values for OnMediaEnd(). // Returns the "default" values for OnMediaEnd().
OnMediaEndParameters GetDefaultOnMediaEndParams(); OnMediaEndParameters GetDefaultOnMediaEndParams();

View File

@ -1811,6 +1811,27 @@ size_t DTSSpecific::ComputeSizeInternal() {
sizeof(kDdtsExtraData); sizeof(kDdtsExtraData);
} }
UDTSSpecific::UDTSSpecific() = default;
UDTSSpecific::~UDTSSpecific() = default;
FourCC UDTSSpecific::BoxType() const {
return FOURCC_udts;
}
bool UDTSSpecific::ReadWriteInternal(BoxBuffer* buffer) {
RCHECK(ReadWriteHeaderInternal(buffer) &&
buffer->ReadWriteVector(
&data, buffer->Reading() ? buffer->BytesLeft() : data.size()));
return true;
}
size_t UDTSSpecific::ComputeSizeInternal() {
// This box is optional. Skip it if not initialized.
if (data.empty())
return 0;
return HeaderSize() + data.size();
}
AC3Specific::AC3Specific() = default; AC3Specific::AC3Specific() = default;
AC3Specific::~AC3Specific() = default; AC3Specific::~AC3Specific() = default;
@ -1983,6 +2004,7 @@ bool AudioSampleEntry::ReadWriteInternal(BoxBuffer* buffer) {
RCHECK(buffer->TryReadWriteChild(&esds)); RCHECK(buffer->TryReadWriteChild(&esds));
RCHECK(buffer->TryReadWriteChild(&ddts)); RCHECK(buffer->TryReadWriteChild(&ddts));
RCHECK(buffer->TryReadWriteChild(&udts));
RCHECK(buffer->TryReadWriteChild(&dac3)); RCHECK(buffer->TryReadWriteChild(&dac3));
RCHECK(buffer->TryReadWriteChild(&dec3)); RCHECK(buffer->TryReadWriteChild(&dec3));
RCHECK(buffer->TryReadWriteChild(&dac4)); RCHECK(buffer->TryReadWriteChild(&dac4));
@ -2014,7 +2036,7 @@ size_t AudioSampleEntry::ComputeSizeInternal() {
sizeof(samplesize) + sizeof(samplerate) + sinf.ComputeSize() + sizeof(samplesize) + sizeof(samplerate) + sinf.ComputeSize() +
esds.ComputeSize() + ddts.ComputeSize() + dac3.ComputeSize() + esds.ComputeSize() + ddts.ComputeSize() + dac3.ComputeSize() +
dec3.ComputeSize() + dops.ComputeSize() + dfla.ComputeSize() + dec3.ComputeSize() + dops.ComputeSize() + dfla.ComputeSize() +
dac4.ComputeSize() + mhac.ComputeSize() + dac4.ComputeSize() + mhac.ComputeSize() + udts.ComputeSize() +
// Reserved and predefined bytes. // Reserved and predefined bytes.
6 + 8 + // 6 + 8 bytes reserved. 6 + 8 + // 6 + 8 bytes reserved.
4; // 4 bytes predefined. 4; // 4 bytes predefined.

View File

@ -334,6 +334,12 @@ struct DTSSpecific : Box {
std::vector<uint8_t> extra_data; std::vector<uint8_t> extra_data;
}; };
struct UDTSSpecific : Box {
DECLARE_BOX_METHODS(UDTSSpecific);
std::vector<uint8_t> data;
};
struct AC3Specific : Box { struct AC3Specific : Box {
DECLARE_BOX_METHODS(AC3Specific); DECLARE_BOX_METHODS(AC3Specific);
@ -396,6 +402,7 @@ struct AudioSampleEntry : Box {
ElementaryStreamDescriptor esds; ElementaryStreamDescriptor esds;
DTSSpecific ddts; DTSSpecific ddts;
UDTSSpecific udts;
AC3Specific dac3; AC3Specific dac3;
EC3Specific dec3; EC3Specific dec3;
AC4Specific dac4; AC4Specific dac4;

View File

@ -417,6 +417,16 @@ class BoxDefinitionsTestGeneral : public testing::Test {
ddts->pcm_sample_depth = 24; ddts->pcm_sample_depth = 24;
} }
void Fill(UDTSSpecific* udts) {
const uint8_t kUdtsData[] = {0x01, 0x20, 0x00, 0x00, 0x0, 0x3F, 0x80, 0x00};
udts->data.assign(kUdtsData, kUdtsData + std::size(kUdtsData));
}
void Modify(UDTSSpecific* udts) {
const uint8_t kUdtsData[] = {0x01, 0x20, 0x01, 0x80, 0xA, 0x3F, 0x80, 0x00};
udts->data.assign(kUdtsData, kUdtsData + std::size(kUdtsData));
}
void Fill(AC3Specific* dac3) { void Fill(AC3Specific* dac3) {
const uint8_t kAc3Data[] = {0x50, 0x11, 0x60}; const uint8_t kAc3Data[] = {0x50, 0x11, 0x60};
dac3->data.assign(kAc3Data, kAc3Data + std::size(kAc3Data)); dac3->data.assign(kAc3Data, kAc3Data + std::size(kAc3Data));
@ -1217,6 +1227,20 @@ TEST_F(BoxDefinitionsTest, DTSSampleEntry) {
ASSERT_EQ(entry, entry_readback); ASSERT_EQ(entry, entry_readback);
} }
TEST_F(BoxDefinitionsTest, UDTSSampleEntry) {
AudioSampleEntry entry;
entry.format = FOURCC_dtsx;
entry.data_reference_index = 2;
entry.channelcount = 6;
entry.samplesize = 16;
entry.samplerate = 48000;
Fill(&entry.udts);
entry.Write(this->buffer_.get());
AudioSampleEntry entry_readback;
ASSERT_TRUE(ReadBack(&entry_readback));
ASSERT_EQ(entry, entry_readback);
}
TEST_F(BoxDefinitionsTest, AC3SampleEntry) { TEST_F(BoxDefinitionsTest, AC3SampleEntry) {
AudioSampleEntry entry; AudioSampleEntry entry;
entry.format = FOURCC_ac_3; entry.format = FOURCC_ac_3;

View File

@ -90,6 +90,8 @@ Codec FourCCToCodec(FourCC fourcc) {
return kCodecDTSL; return kCodecDTSL;
case FOURCC_dtse: case FOURCC_dtse:
return kCodecDTSE; return kCodecDTSE;
case FOURCC_dtsx:
return kCodecDTSX;
case FOURCC_dtsp: case FOURCC_dtsp:
return kCodecDTSP; return kCodecDTSP;
case FOURCC_dtsm: case FOURCC_dtsm:
@ -491,6 +493,9 @@ bool MP4MediaParser::ParseMoov(BoxReader* reader) {
max_bitrate = entry.ddts.max_bitrate; max_bitrate = entry.ddts.max_bitrate;
avg_bitrate = entry.ddts.avg_bitrate; avg_bitrate = entry.ddts.avg_bitrate;
break; break;
case FOURCC_dtsx:
codec_config = entry.udts.data;
break;
case FOURCC_ac_3: case FOURCC_ac_3:
codec_config = entry.dac3.data; codec_config = entry.dac3.data;
num_channels = static_cast<uint8_t>(GetAc3NumChannels(codec_config)); num_channels = static_cast<uint8_t>(GetAc3NumChannels(codec_config));

View File

@ -86,6 +86,8 @@ FourCC CodecToFourCC(Codec codec, H26xStreamFormat h26x_stream_format) {
return FOURCC_dtse; return FOURCC_dtse;
case kCodecDTSM: case kCodecDTSM:
return FOURCC_dtsm; return FOURCC_dtsm;
case kCodecDTSX:
return FOURCC_dtsx;
case kCodecEAC3: case kCodecEAC3:
return FOURCC_ec_3; return FOURCC_ec_3;
case kCodecAC4: case kCodecAC4:
@ -498,6 +500,9 @@ bool MP4Muxer::GenerateAudioTrak(const AudioStreamInfo* audio_info,
audio.ddts.sampling_frequency = audio_info->sampling_frequency(); audio.ddts.sampling_frequency = audio_info->sampling_frequency();
audio.ddts.pcm_sample_depth = audio_info->sample_bits(); audio.ddts.pcm_sample_depth = audio_info->sample_bits();
break; break;
case kCodecDTSX:
audio.udts.data = audio_info->codec_config();
break;
case kCodecAC3: case kCodecAC3:
audio.dac3.data = audio_info->codec_config(); audio.dac3.data = audio_info->codec_config();
break; break;

View File

@ -118,3 +118,11 @@ sintel-1024x436.mp4 - First 6 seconds of Sintel stream.
// http://download.opencontent.netflix.com/?prefix=TechblogAssets/Sparks/Sparks_DolbyVision_P3D65_PQ_5994fps_4096x2160_LtRt_IMF_20170214/ // http://download.opencontent.netflix.com/?prefix=TechblogAssets/Sparks/Sparks_DolbyVision_P3D65_PQ_5994fps_4096x2160_LtRt_IMF_20170214/
sparks_dovi_5.mp4 - Dolby Vision profile 5 sparks_dovi_5.mp4 - Dolby Vision profile 5
sparks_dovi_8.mp4 - Dolby Vision profile 8 sparks_dovi_8.mp4 - Dolby Vision profile 8
// DTS:X Profile 2 5.1 channel audio
bear-dtsx.mp4:
Generated using ffmpeg and proprietary encoder:
ffmpeg -i bear.aiff -i bear.aiff -i bear.aiff \
-filter_complex "[0:a][1:a][2:a]concat=n=3:v=0:a=1[out]" \
-ar 48000 -map "[out]" -y bear.wav
dtsx-ott-enc -w -o bear-dtsx.mp4 -c 5.1 bear.wav bear.wav bear.wav

Binary file not shown.

View File

@ -59,6 +59,8 @@ std::string RoleToText(AdaptationSet::Role role) {
return "commentary"; return "commentary";
case AdaptationSet::kRoleDub: case AdaptationSet::kRoleDub:
return "dub"; return "dub";
case AdaptationSet::kRoleForcedSubtitle:
return "forced-subtitle";
case AdaptationSet::kRoleDescription: case AdaptationSet::kRoleDescription:
return "description"; return "description";
default: default:

View File

@ -43,6 +43,7 @@ class AdaptationSet {
kRoleSupplementary, kRoleSupplementary,
kRoleCommentary, kRoleCommentary,
kRoleDub, kRoleDub,
kRoleForcedSubtitle,
kRoleDescription kRoleDescription
}; };

View File

@ -213,6 +213,11 @@ message MediaInfo {
// Equal to the target segment duration times the reference time scale. // Equal to the target segment duration times the reference time scale.
optional uint64 segment_duration = 25; optional uint64 segment_duration = 25;
// Marks stream as a Forced Narrative subtitle stream, indicated using
// forced-subtitle role in DASH
// and FORCED=YES in HLS
optional bool forced_subtitle = 26 [default = false];
// stream index for consistent ordering of streams // stream index for consistent ordering of streams
optional uint32 index = 28; optional uint32 index = 28;

View File

@ -59,6 +59,8 @@ AdaptationSet::Role RoleFromString(const std::string& role_str) {
return AdaptationSet::Role::kRoleCommentary; return AdaptationSet::Role::kRoleCommentary;
if (role_str == "dub") if (role_str == "dub")
return AdaptationSet::Role::kRoleDub; return AdaptationSet::Role::kRoleDub;
if (role_str == "forced-subtitle")
return AdaptationSet::Role::kRoleForcedSubtitle;
if (role_str == "description") if (role_str == "description")
return AdaptationSet::Role::kRoleDescription; return AdaptationSet::Role::kRoleDescription;
return AdaptationSet::Role::kRoleUnknown; return AdaptationSet::Role::kRoleUnknown;

View File

@ -49,6 +49,9 @@ typedef MediaInfo::VideoInfo VideoInfo;
namespace { namespace {
const char kEC3Codec[] = "ec-3"; const char kEC3Codec[] = "ec-3";
const char kAC4Codec[] = "ac-4"; const char kAC4Codec[] = "ac-4";
const char kDTSCCodec[] = "dtsc";
const char kDTSECodec[] = "dtse";
const char kDTSXCodec[] = "dtsx";
std::string RangeToString(const Range& range) { std::string RangeToString(const Range& range) {
return absl::StrFormat("%u-%u", range.begin(), range.end()); return absl::StrFormat("%u-%u", range.begin(), range.end());
@ -680,6 +683,18 @@ bool RepresentationXmlNode::AddAudioChannelInfo(const AudioInfo& audio_info) {
"1"); "1");
} }
return ret; return ret;
} else if (audio_info.codec() == kDTSCCodec ||
audio_info.codec() == kDTSECodec) {
audio_channel_config_value =
absl::StrFormat("%u", audio_info.num_channels());
audio_channel_config_scheme =
"tag:dts.com,2014:dash:audio_channel_configuration:2012";
} else if (audio_info.codec() == kDTSXCodec) {
const auto& codec_data = audio_info.codec_specific_data();
audio_channel_config_value =
absl::StrFormat("%08X", codec_data.channel_mask());
audio_channel_config_scheme =
"tag:dts.com,2018:uhd:audio_channel_configuration";
} else { } else {
audio_channel_config_value = audio_channel_config_value =
absl::StrFormat("%u", audio_info.num_channels()); absl::StrFormat("%u", audio_info.num_channels());

View File

@ -766,5 +766,61 @@ TEST_F(LowLatencySegmentTest, LowLatencySegmentTemplate) {
"</Representation>")); "</Representation>"));
} }
TEST(XmlNodeTest, AddDTSCAudioInfo) {
MediaInfo::AudioInfo audio_info;
audio_info.set_codec("dtsc");
audio_info.set_sampling_frequency(48000);
audio_info.set_num_channels(6);
RepresentationXmlNode representation;
ASSERT_TRUE(representation.AddAudioInfo(audio_info));
EXPECT_THAT(
representation,
XmlNodeEqual(
"<Representation audioSamplingRate=\"48000\">\n"
" <AudioChannelConfiguration\n"
" schemeIdUri=\n"
" \"tag:dts.com,2014:dash:audio_channel_configuration:2012\"\n"
" value=\"6\"/>\n"
"</Representation>\n"));
}
TEST(XmlNodeTest, AddDTSEAudioInfo) {
MediaInfo::AudioInfo audio_info;
audio_info.set_codec("dtse");
audio_info.set_sampling_frequency(48000);
audio_info.set_num_channels(6);
RepresentationXmlNode representation;
ASSERT_TRUE(representation.AddAudioInfo(audio_info));
EXPECT_THAT(
representation,
XmlNodeEqual(
"<Representation audioSamplingRate=\"48000\">\n"
" <AudioChannelConfiguration\n"
" schemeIdUri=\n"
" \"tag:dts.com,2014:dash:audio_channel_configuration:2012\"\n"
" value=\"6\"/>\n"
"</Representation>\n"));
}
TEST(XmlNodeTest, AddDTSXAudioInfo) {
MediaInfo::AudioInfo audio_info;
audio_info.set_codec("dtsx");
audio_info.set_sampling_frequency(48000);
audio_info.mutable_codec_specific_data()->set_channel_mask(0x3F);
RepresentationXmlNode representation;
ASSERT_TRUE(representation.AddAudioInfo(audio_info));
EXPECT_THAT(
representation,
XmlNodeEqual("<Representation audioSamplingRate=\"48000\">\n"
" <AudioChannelConfiguration\n"
" schemeIdUri=\n"
" \"tag:dts.com,2018:uhd:audio_channel_configuration\"\n"
" value=\"0000003F\"/>\n"
"</Representation>\n"));
}
} // namespace xml } // namespace xml
} // namespace shaka } // namespace shaka

View File

@ -69,6 +69,7 @@ MuxerListenerFactory::StreamData ToMuxerListenerData(
data.hls_playlist_name = stream.hls_playlist_name; data.hls_playlist_name = stream.hls_playlist_name;
data.hls_iframe_playlist_name = stream.hls_iframe_playlist_name; data.hls_iframe_playlist_name = stream.hls_iframe_playlist_name;
data.hls_characteristics = stream.hls_characteristics; data.hls_characteristics = stream.hls_characteristics;
data.forced_subtitle = stream.forced_subtitle;
data.hls_only = stream.hls_only; data.hls_only = stream.hls_only;
data.dash_accessiblities = stream.dash_accessiblities; data.dash_accessiblities = stream.dash_accessiblities;