Convert text WebVTT output to muxer.

Now text-based WebVTT also uses the generic media pipeline.  This
converts the WebVttTextOutputHandler to a WebVttMuxer to be more
consistent with the other muxer types.

This also allows choosing between single-segment text and multi-segment.
Before, we would generate both and use single-segment for DASH and
multi-segment for HLS; but now you can choose between either and either
are supported in both DASH and HLS.

Change-Id: I6f7edda09e01b5f40e819290d3fe6e88677018d9
This commit is contained in:
Jacob Trimble 2020-07-30 14:26:45 -07:00
parent 2909ca0c77
commit 56908a83a7
52 changed files with 508 additions and 434 deletions

View File

@ -13,6 +13,7 @@
#include "packager/media/formats/mp4/mp4_muxer.h" #include "packager/media/formats/mp4/mp4_muxer.h"
#include "packager/media/formats/packed_audio/packed_audio_writer.h" #include "packager/media/formats/packed_audio/packed_audio_writer.h"
#include "packager/media/formats/webm/webm_muxer.h" #include "packager/media/formats/webm/webm_muxer.h"
#include "packager/media/formats/webvtt/webvtt_muxer.h"
#include "packager/packager.h" #include "packager/packager.h"
namespace shaka { namespace shaka {
@ -20,9 +21,9 @@ namespace media {
MuxerFactory::MuxerFactory(const PackagingParams& packaging_params) MuxerFactory::MuxerFactory(const PackagingParams& packaging_params)
: mp4_params_(packaging_params.mp4_output_params), : mp4_params_(packaging_params.mp4_output_params),
temp_dir_(packaging_params.temp_dir),
transport_stream_timestamp_offset_ms_( transport_stream_timestamp_offset_ms_(
packaging_params.transport_stream_timestamp_offset_ms), packaging_params.transport_stream_timestamp_offset_ms) {}
temp_dir_(packaging_params.temp_dir) {}
std::shared_ptr<Muxer> MuxerFactory::CreateMuxer( std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
MediaContainerName output_format, MediaContainerName output_format,
@ -48,6 +49,9 @@ std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
case CONTAINER_WEBM: case CONTAINER_WEBM:
muxer = std::make_shared<webm::WebMMuxer>(options); muxer = std::make_shared<webm::WebMMuxer>(options);
break; break;
case CONTAINER_WEBVTT:
muxer = std::make_shared<webvtt::WebVttMuxer>(options);
break;
case CONTAINER_MPEG2TS: case CONTAINER_MPEG2TS:
muxer = std::make_shared<mp2t::TsMuxer>(options); muxer = std::make_shared<mp2t::TsMuxer>(options);
break; break;

View File

@ -42,13 +42,17 @@ class MuxerFactory {
/// this will replace the clock for all muxers created after this call. /// this will replace the clock for all muxers created after this call.
void OverrideClock(base::Clock* clock); void OverrideClock(base::Clock* clock);
void SetTsStreamOffset(uint32_t offset_ms) {
transport_stream_timestamp_offset_ms_ = offset_ms;
}
private: private:
MuxerFactory(const MuxerFactory&) = delete; MuxerFactory(const MuxerFactory&) = delete;
MuxerFactory& operator=(const MuxerFactory&) = delete; MuxerFactory& operator=(const MuxerFactory&) = delete;
const Mp4OutputParams mp4_params_; const Mp4OutputParams mp4_params_;
const uint32_t transport_stream_timestamp_offset_ms_ = 0;
const std::string temp_dir_; const std::string temp_dir_;
uint32_t transport_stream_timestamp_offset_ms_ = 0;
base::Clock* clock_ = nullptr; base::Clock* clock_ = nullptr;
}; };

View File

@ -833,6 +833,21 @@ class PackagerFunctionalTest(PackagerAppTest):
self.assertPackageSuccess(streams, flags) self.assertPackageSuccess(streams, flags)
self._CheckTestResults('segmented-webvtt-with-language-override') self._CheckTestResults('segmented-webvtt-with-language-override')
def testSegmentedWebVttText(self):
streams = self._GetStreams(
['text'], test_files=['bear-english.vtt'], segmented=True)
flags = self._GetFlags(output_hls=True, output_dash=True)
self.assertPackageSuccess(streams, flags)
self._CheckTestResults('segmented-webvtt-text')
def testSingleFileWebVttText(self):
streams = self._GetStreams(['text'], test_files=['bear-english.vtt'])
flags = self._GetFlags(output_hls=True, output_dash=True)
self.assertPackageSuccess(streams, flags)
self._CheckTestResults('single-file-webvtt-text')
def testMp4TrailingMoov(self): def testMp4TrailingMoov(self):
self.assertPackageSuccess( self.assertPackageSuccess(
self._GetStreams(['audio', 'video'], self._GetStreams(['audio', 'video'],
@ -1930,7 +1945,8 @@ class PackagerCommandParsingTest(PackagerAppTest):
packaging_result = self.packager.Package(audio_video_stream + text_stream, packaging_result = self.packager.Package(audio_video_stream + text_stream,
self._GetFlags()) self._GetFlags())
# Expect the test to fail but we do not expect a crash. # Expect the test to fail but we do not expect a crash.
self.assertEqual(packaging_result, 1) self.assertNotEqual(packaging_result, 0)
self.assertLess(packaging_result, 10)
def testInconsistentOutputAndSegmentTemplateFormat(self): def testInconsistentOutputAndSegmentTemplateFormat(self):
test_file = os.path.join(self.test_data_dir, 'bear-640x360.mp4') test_file = os.path.join(self.test_data_dir, 'bear-640x360.mp4')

View File

@ -8,3 +8,4 @@ Yup, that's a bear, eh.
00:00:01.000 --> 00:00:04.700 00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff. He 's... um... doing bear-like stuff.

View File

@ -4,7 +4,7 @@
<Period id="0"> <Period id="0">
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true"> <AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="caption"/> <Role schemeIdUri="urn:mpeg:dash:role:2011" value="caption"/>
<Representation id="0" bandwidth="256" mimeType="text/vtt"> <Representation id="0" bandwidth="273" mimeType="text/vtt">
<BaseURL>bear-english-text.vtt</BaseURL> <BaseURL>bear-english-text.vtt</BaseURL>
</Representation> </Representation>
</AdaptationSet> </AdaptationSet>

View File

@ -3,9 +3,9 @@
#EXT-X-INDEPENDENT-SEGMENTS #EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_1",AUTOSELECT=YES,CHANNELS="2" #EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="bear-english-text.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES #EXT-X-MEDIA:TYPE=SUBTITLES,URI="bear-english-text.m3u8",GROUP-ID="default-text-group",NAME="stream_2",AUTOSELECT=YES
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group" #EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
bear-640x360-video.m3u8 bear-640x360-video.m3u8

View File

@ -3,9 +3,9 @@
#EXT-X-INDEPENDENT-SEGMENTS #EXT-X-INDEPENDENT-SEGMENTS
#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=AUDIO,URI="stream_0.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",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_3.m3u8",GROUP-ID="default-text-group",NAME="stream_3",AUTOSELECT=YES
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group" #EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
stream_2.m3u8 stream_1.m3u8

View File

@ -3,14 +3,11 @@
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test> ## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2 #EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD #EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:1.000, #EXT-X-MAP:URI="bear-640x360-audio-init.mp4"
bear-english-text-1.vtt #EXTINF:1.022,
#EXTINF:1.000, bear-640x360-audio-1.m4s
bear-english-text-2.vtt #EXTINF:0.998,
#EXTINF:1.000, bear-640x360-audio-2.m4s
bear-english-text-3.vtt #EXTINF:0.720,
#EXTINF:1.000, bear-640x360-audio-3.m4s
bear-english-text-4.vtt
#EXTINF:1.000,
bear-english-text-5.vtt
#EXT-X-ENDLIST #EXT-X-ENDLIST

View File

@ -3,11 +3,11 @@
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test> ## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2 #EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD #EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="bear-640x360-audio-init.mp4" #EXT-X-MAP:URI="bear-640x360-video-init.mp4"
#EXTINF:1.022, #EXTINF:1.001,
bear-640x360-audio-1.m4s bear-640x360-video-1.m4s
#EXTINF:0.998, #EXTINF:1.001,
bear-640x360-audio-2.m4s bear-640x360-video-2.m4s
#EXTINF:0.720, #EXTINF:0.734,
bear-640x360-audio-3.m4s bear-640x360-video-3.m4s
#EXT-X-ENDLIST #EXT-X-ENDLIST

View File

@ -1,13 +0,0 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="bear-640x360-video-init.mp4"
#EXTINF:1.001,
bear-640x360-video-1.m4s
#EXTINF:1.001,
bear-640x360-video-2.m4s
#EXTINF:0.734,
bear-640x360-video-3.m4s
#EXT-X-ENDLIST

View File

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

View File

@ -3,9 +3,9 @@
#EXT-X-INDEPENDENT-SEGMENTS #EXT-X-INDEPENDENT-SEGMENTS
#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=AUDIO,URI="stream_0.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
#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-MEDIA:TYPE=SUBTITLES,URI="stream_2.m3u8",GROUP-ID="default-text-group",NAME="stream_2",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,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group" #EXT-X-STREAM-INF:BANDWIDTH=1217518,AVERAGE-BANDWIDTH=1117319,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
stream_2.m3u8 stream_1.m3u8

View File

@ -3,14 +3,10 @@
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test> ## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2 #EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD #EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:1.000, #EXTINF:1.045,
bear-english-text-1.vtt bear-640x360-audio-1.ts
#EXTINF:1.000, #EXTINF:0.998,
bear-english-text-2.vtt bear-640x360-audio-2.ts
#EXTINF:1.000, #EXTINF:0.720,
bear-english-text-3.vtt bear-640x360-audio-3.ts
#EXTINF:1.000,
bear-english-text-4.vtt
#EXTINF:1.000,
bear-english-text-5.vtt
#EXT-X-ENDLIST #EXT-X-ENDLIST

View File

@ -3,10 +3,10 @@
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test> ## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2 #EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD #EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:1.045, #EXTINF:1.001,
bear-640x360-audio-1.ts bear-640x360-video-1.ts
#EXTINF:0.998, #EXTINF:1.001,
bear-640x360-audio-2.ts bear-640x360-video-2.ts
#EXTINF:0.720, #EXTINF:0.734,
bear-640x360-audio-3.ts bear-640x360-video-3.ts
#EXT-X-ENDLIST #EXT-X-ENDLIST

View File

@ -3,10 +3,14 @@
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test> ## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-TARGETDURATION:2 #EXT-X-TARGETDURATION:2
#EXT-X-PLAYLIST-TYPE:VOD #EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:1.001, #EXTINF:1.000,
bear-640x360-video-1.ts bear-english-text-1.vtt
#EXTINF:1.001, #EXTINF:1.000,
bear-640x360-video-2.ts bear-english-text-2.vtt
#EXTINF:0.734, #EXTINF:1.000,
bear-640x360-video-3.ts bear-english-text-3.vtt
#EXTINF:1.000,
bear-english-text-4.vtt
#EXTINF:1.000,
bear-english-text-5.vtt
#EXT-X-ENDLIST #EXT-X-ENDLIST

View File

@ -0,0 +1,9 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:00.000 --> 00:00:00.800
Yup, that's a bear, eh.

View File

@ -0,0 +1,9 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,9 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,9 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,9 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,6 @@
#EXTM3U
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/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-live:2011" minBufferTime="PT2S" type="dynamic" publishTime="some_time" availabilityStartTime="some_time" minimumUpdatePeriod="PT5S" timeShiftBufferDepth="PT1800S">
<Period id="0" start="PT0S">
<AdaptationSet id="0" contentType="text" segmentAlignment="true">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
<Representation id="0" bandwidth="1216" mimeType="text/vtt">
<SegmentTemplate timescale="1000" media="bear-english-text-$Number$.vtt" startNumber="1">
<SegmentTimeline>
<S t="0" d="1000" r="4"/>
</SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -3,4 +3,4 @@
#EXT-X-INDEPENDENT-SEGMENTS #EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",LANGUAGE="pt",NAME="stream_0",AUTOSELECT=YES #EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_1.m3u8",GROUP-ID="default-text-group",LANGUAGE="pt",NAME="stream_1",AUTOSELECT=YES

View File

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

View File

@ -0,0 +1,12 @@
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
STYLE
::cue { color:lime }
00:00:00.000 --> 00:00:00.800
Yup, that's a bear, eh.
00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff.

View File

@ -0,0 +1,6 @@
#EXTM3U
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/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="PT4.699999809265137S">
<Period id="0">
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
<Representation id="0" bandwidth="353" 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/google/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

@ -8,3 +8,4 @@ Yup, that's a bear, eh.
00:00:01.000 --> 00:00:04.700 00:00:01.000 --> 00:00:04.700
He 's... um... doing bear-like stuff. He 's... um... doing bear-like stuff.

View File

@ -3,7 +3,8 @@
<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.7360665798187256S"> <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.7360665798187256S">
<Period id="0"> <Period id="0">
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true"> <AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
<Representation id="0" bandwidth="256" mimeType="text/vtt"> <Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
<Representation id="0" bandwidth="273" mimeType="text/vtt">
<BaseURL>bear-english-text.vtt</BaseURL> <BaseURL>bear-english-text.vtt</BaseURL>
</Representation> </Representation>
</AdaptationSet> </AdaptationSet>

View File

@ -391,7 +391,8 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
time_scale_ = time_scale; time_scale_ = time_scale;
media_info_ = media_info; media_info_ = media_info;
language_ = GetLanguage(media_info); language_ = GetLanguage(media_info);
use_byte_range_ = !media_info_.has_segment_template_url(); use_byte_range_ = !media_info_.has_segment_template_url() &&
media_info_.container_type() != MediaInfo::CONTAINER_TEXT;
characteristics_ = characteristics_ =
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());

View File

@ -68,7 +68,11 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
return FinalizeSegment(stream_data->stream_index, segment_info); return FinalizeSegment(stream_data->stream_index, segment_info);
} }
case StreamDataType::kMediaSample: case StreamDataType::kMediaSample:
return AddSample(stream_data->stream_index, *stream_data->media_sample); return AddMediaSample(stream_data->stream_index,
*stream_data->media_sample);
case StreamDataType::kTextSample:
return AddTextSample(stream_data->stream_index,
*stream_data->text_sample);
case StreamDataType::kCueEvent: case StreamDataType::kCueEvent:
if (muxer_listener_) { if (muxer_listener_) {
const int64_t time_scale = const int64_t time_scale =
@ -99,6 +103,14 @@ Status Muxer::OnFlushRequest(size_t input_stream_index) {
return Finalize(); return Finalize();
} }
Status Muxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
return Status::OK;
}
Status Muxer::AddTextSample(size_t stream_id, const TextSample& sample) {
return Status::OK;
}
Status Muxer::ReinitializeMuxer(int64_t timestamp) { Status Muxer::ReinitializeMuxer(int64_t timestamp) {
if (muxer_listener_ && streams_.back()->is_encrypted()) { if (muxer_listener_ && streams_.back()->is_encrypted()) {
const EncryptionConfig& encryption_config = const EncryptionConfig& encryption_config =

View File

@ -83,10 +83,13 @@ class Muxer : public MediaHandler {
// Final clean up. // Final clean up.
virtual Status Finalize() = 0; virtual Status Finalize() = 0;
// Add a new sample. // Add a new media sample. This does nothing by default; so subclasses that
virtual Status AddSample( // handle media samples will need to replace this.
size_t stream_id, virtual Status AddMediaSample(size_t stream_id, const MediaSample& sample);
const MediaSample& sample) = 0;
// Add a new text sample. This does nothing by default; so subclasses that
// handle text samples will need to replace this.
virtual Status AddTextSample(size_t stream_id, const TextSample& sample);
// Finalize the segment or subsegment. // Finalize the segment or subsegment.
virtual Status FinalizeSegment( virtual Status FinalizeSegment(

View File

@ -73,8 +73,14 @@ void MpdNotifyMuxerListener::OnMediaStart(
} }
for (const std::string& accessibility : accessibilities_) for (const std::string& accessibility : accessibilities_)
media_info->add_dash_accessibilities(accessibility); media_info->add_dash_accessibilities(accessibility);
for (const std::string& role : roles_) if (roles_.empty() && stream_info.stream_type() == kStreamText) {
media_info->add_dash_roles(role); // If there aren't any roles, default to "subtitle" since some apps may
// require it to distinguish between subtitle/caption.
media_info->add_dash_roles("subtitle");
} else {
for (const std::string& role : roles_)
media_info->add_dash_roles(role);
}
if (is_encrypted_) { if (is_encrypted_) {
internal::SetContentProtectionFields(protection_scheme_, default_key_id_, internal::SetContentProtectionFields(protection_scheme_, default_key_id_,

View File

@ -166,9 +166,8 @@ void AddAudioInfo(const AudioStreamInfo* audio_stream_info,
void AddTextInfo(const TextStreamInfo& text_stream_info, void AddTextInfo(const TextStreamInfo& text_stream_info,
MediaInfo* media_info) { MediaInfo* media_info) {
// For now, set everything as subtitle. // TODO(modmaker): Set kind.
MediaInfo::TextInfo* text_info = media_info->mutable_text_info(); MediaInfo::TextInfo* text_info = media_info->mutable_text_info();
text_info->set_type(MediaInfo::TextInfo::SUBTITLE);
text_info->set_codec(text_stream_info.codec_string()); text_info->set_codec(text_stream_info.codec_string());
text_info->set_language(text_stream_info.language()); text_info->set_language(text_stream_info.language());
} }
@ -234,8 +233,9 @@ bool GenerateMediaInfo(const MuxerOptions& muxer_options,
SetMediaInfoMuxerOptions(muxer_options, media_info); SetMediaInfoMuxerOptions(muxer_options, media_info);
SetMediaInfoStreamInfo(stream_info, media_info); SetMediaInfoStreamInfo(stream_info, media_info);
media_info->set_reference_time_scale(reference_time_scale);
SetMediaInfoContainerType(container_type, media_info); SetMediaInfoContainerType(container_type, media_info);
if (reference_time_scale > 0)
media_info->set_reference_time_scale(reference_time_scale);
if (muxer_options.bandwidth > 0) if (muxer_options.bandwidth > 0)
media_info->set_bandwidth(muxer_options.bandwidth); media_info->set_bandwidth(muxer_options.bandwidth);

View File

@ -32,15 +32,15 @@ Status TsMuxer::Finalize() {
return segmenter_->Finalize(); return segmenter_->Finalize();
} }
Status TsMuxer::AddSample(size_t stream_id, const MediaSample& sample) { Status TsMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
DCHECK_EQ(stream_id, 0u); DCHECK_EQ(stream_id, 0u);
if (num_samples_ < 2) { if (num_samples_ < 2) {
sample_durations_[num_samples_] = sample.duration() * kTsTimescale / sample_durations_[num_samples_] =
streams().front()->time_scale(); sample.duration() * kTsTimescale / streams().front()->time_scale();
if (num_samples_ == 1 && muxer_listener()) if (num_samples_ == 1 && muxer_listener())
muxer_listener()->OnSampleDurationReady(sample_durations_[num_samples_]); muxer_listener()->OnSampleDurationReady(sample_durations_[num_samples_]);
num_samples_++; num_samples_++;
} }
return segmenter_->AddSample(sample); return segmenter_->AddSample(sample);
} }

View File

@ -26,8 +26,7 @@ class TsMuxer : public Muxer {
// Muxer implementation. // Muxer implementation.
Status InitializeMuxer() override; Status InitializeMuxer() override;
Status Finalize() override; Status Finalize() override;
Status AddSample(size_t stream_id, Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
const MediaSample& sample) override;
Status FinalizeSegment(size_t stream_id, Status FinalizeSegment(size_t stream_id,
const SegmentInfo& sample) override; const SegmentInfo& sample) override;

View File

@ -181,7 +181,7 @@ Status MP4Muxer::Finalize() {
return Status::OK; return Status::OK;
} }
Status MP4Muxer::AddSample(size_t stream_id, const MediaSample& sample) { Status MP4Muxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
if (to_be_initialized_) { if (to_be_initialized_) {
RETURN_IF_ERROR(UpdateEditListOffsetFromSample(sample)); RETURN_IF_ERROR(UpdateEditListOffsetFromSample(sample));
RETURN_IF_ERROR(DelayInitializeMuxer()); RETURN_IF_ERROR(DelayInitializeMuxer());

View File

@ -39,7 +39,7 @@ class MP4Muxer : public Muxer {
// Muxer implementation overrides. // Muxer implementation overrides.
Status InitializeMuxer() override; Status InitializeMuxer() override;
Status Finalize() override; Status Finalize() override;
Status AddSample(size_t stream_id, const MediaSample& sample) override; Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
Status FinalizeSegment(size_t stream_id, Status FinalizeSegment(size_t stream_id,
const SegmentInfo& segment_info) override; const SegmentInfo& segment_info) override;

View File

@ -58,8 +58,8 @@ Status PackedAudioWriter::Finalize() {
return Status::OK; return Status::OK;
} }
Status PackedAudioWriter::AddSample(size_t stream_id, Status PackedAudioWriter::AddMediaSample(size_t stream_id,
const MediaSample& sample) { const MediaSample& sample) {
DCHECK_EQ(stream_id, 0u); DCHECK_EQ(stream_id, 0u);
return segmenter_->AddSample(sample); return segmenter_->AddSample(sample);
} }

View File

@ -35,7 +35,7 @@ class PackedAudioWriter : public Muxer {
// Muxer implementations. // Muxer implementations.
Status InitializeMuxer() override; Status InitializeMuxer() override;
Status Finalize() override; Status Finalize() override;
Status AddSample(size_t stream_id, const MediaSample& sample) override; Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
Status FinalizeSegment(size_t stream_id, const SegmentInfo& sample) override; Status FinalizeSegment(size_t stream_id, const SegmentInfo& sample) override;
Status WriteSegment(const std::string& segment_path, Status WriteSegment(const std::string& segment_path,

View File

@ -58,7 +58,7 @@ Status WebMMuxer::Finalize() {
return Status::OK; return Status::OK;
} }
Status WebMMuxer::AddSample(size_t stream_id, const MediaSample& sample) { Status WebMMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
DCHECK(segmenter_); DCHECK(segmenter_);
DCHECK_EQ(stream_id, 0u); DCHECK_EQ(stream_id, 0u);
if (sample.pts() < 0) { if (sample.pts() < 0) {

View File

@ -26,7 +26,7 @@ class WebMMuxer : public Muxer {
// Muxer implementation overrides. // Muxer implementation overrides.
Status InitializeMuxer() override; Status InitializeMuxer() override;
Status Finalize() override; Status Finalize() override;
Status AddSample(size_t stream_id, const MediaSample& sample) override; Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
Status FinalizeSegment(size_t stream_id, Status FinalizeSegment(size_t stream_id,
const SegmentInfo& segment_info) override; const SegmentInfo& segment_info) override;

View File

@ -19,10 +19,10 @@
'text_readers.h', 'text_readers.h',
'webvtt_file_buffer.cc', 'webvtt_file_buffer.cc',
'webvtt_file_buffer.h', 'webvtt_file_buffer.h',
'webvtt_muxer.cc',
'webvtt_muxer.h',
'webvtt_parser.cc', 'webvtt_parser.cc',
'webvtt_parser.h', 'webvtt_parser.h',
'webvtt_text_output_handler.cc',
'webvtt_text_output_handler.h',
'webvtt_timestamp.cc', 'webvtt_timestamp.cc',
'webvtt_timestamp.h', 'webvtt_timestamp.h',
'webvtt_to_mp4_handler.cc', 'webvtt_to_mp4_handler.cc',
@ -40,8 +40,8 @@
'type': '<(gtest_target_type)', 'type': '<(gtest_target_type)',
'sources': [ 'sources': [
'text_readers_unittest.cc', 'text_readers_unittest.cc',
'webvtt_muxer_unittest.cc',
'webvtt_parser_unittest.cc', 'webvtt_parser_unittest.cc',
'webvtt_text_output_handler_unittest.cc',
'webvtt_timestamp_unittest.cc', 'webvtt_timestamp_unittest.cc',
'webvtt_to_mp4_handler_unittest.cc', 'webvtt_to_mp4_handler_unittest.cc',
], ],

View File

@ -0,0 +1,135 @@
// Copyright 2020 Google LLC. 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/media/formats/webvtt/webvtt_muxer.h"
#include <memory>
#include "packager/file/file.h"
#include "packager/file/file_closer.h"
#include "packager/media/base/muxer_util.h"
#include "packager/media/formats/webvtt/webvtt_timestamp.h"
#include "packager/status_macros.h"
namespace shaka {
namespace media {
namespace webvtt {
namespace {
std::string ToString(const std::vector<uint8_t>& v) {
return std::string(v.begin(), v.end());
}
} // namespace
WebVttMuxer::WebVttMuxer(const MuxerOptions& options) : Muxer(options) {}
WebVttMuxer::~WebVttMuxer() {}
Status WebVttMuxer::InitializeMuxer() {
if (streams().size() != 1) {
return Status(error::MUXER_FAILURE, "Incorrect number of streams");
}
// Only initialize the stream once we see a cue to avoid empty files.
muxer_listener()->OnMediaStart(options(), *streams()[0],
streams()[0]->time_scale(),
MuxerListener::kContainerText);
buffer_.reset(
new WebVttFileBuffer(options().transport_stream_timestamp_offset_ms,
ToString(streams()[0]->codec_config())));
last_cue_ms_ = 0;
return Status::OK;
}
Status WebVttMuxer::Finalize() {
const float duration_ms = static_cast<float>(total_duration_ms_);
float duration_seconds = duration_ms / 1000;
// If we haven't seen any segments, this is a single-file. In this case,
// flush the single segment.
MuxerListener::MediaRanges ranges;
if (duration_seconds == 0 && last_cue_ms_ != 0) {
DCHECK(options().segment_template.empty());
duration_seconds = static_cast<float>(last_cue_ms_) / 1000;
uint64_t size;
RETURN_IF_ERROR(WriteToFile(options().output_file_name, &size));
// Insert a dummy value so the HLS generator will generate a segment list.
ranges.subsegment_ranges.emplace_back();
muxer_listener()->OnNewSegment(
options().output_file_name, 0,
duration_seconds * streams()[0]->time_scale(), size);
}
muxer_listener()->OnMediaEnd(ranges, duration_seconds);
return Status::OK;
}
Status WebVttMuxer::AddTextSample(size_t stream_id, const TextSample& sample) {
// Ignore sync samples.
if (sample.payload().empty()) {
return Status::OK;
}
if (sample.id().find('\n') != std::string::npos ||
sample.settings().find('\n') != std::string::npos) {
return Status(error::MUXER_FAILURE,
"Text id/settings cannot contain newlines");
}
last_cue_ms_ = sample.EndTime();
buffer_->Append(sample);
return Status::OK;
}
Status WebVttMuxer::FinalizeSegment(size_t stream_id,
const SegmentInfo& segment_info) {
total_duration_ms_ += segment_info.duration;
const std::string& segment_template = options().segment_template;
DCHECK(!segment_template.empty());
const uint32_t index = segment_index_++;
const uint64_t start = segment_info.start_timestamp;
const uint64_t duration = segment_info.duration;
const uint32_t bandwidth = options().bandwidth;
const std::string filename =
GetSegmentName(segment_template, start, index, bandwidth);
uint64_t size;
RETURN_IF_ERROR(WriteToFile(filename, &size));
muxer_listener()->OnNewSegment(filename, start, duration, size);
return Status::OK;
}
Status WebVttMuxer::WriteToFile(const std::string& filename, uint64_t* size) {
// Write everything to the file before telling the manifest so that the
// file will exist on disk.
std::unique_ptr<File, FileCloser> file(File::Open(filename.c_str(), "w"));
if (!file) {
return Status(error::FILE_FAILURE, "Failed to open " + filename);
}
buffer_->WriteTo(file.get());
buffer_->Reset();
if (!file.release()->Close()) {
return Status(error::FILE_FAILURE, "Failed to close " + filename);
}
if (size) {
*size = File::GetFileSize(filename.c_str());
}
return Status::OK;
}
} // namespace webvtt
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,47 @@
// Copyright 2020 Google LLC. 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_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_
#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_
#include "packager/media/base/buffer_writer.h"
#include "packager/media/base/muxer.h"
#include "packager/media/formats/webvtt/webvtt_file_buffer.h"
namespace shaka {
namespace media {
namespace webvtt {
/// Implements WebVtt Muxer.
class WebVttMuxer : public Muxer {
public:
/// Create a WebMMuxer object from MuxerOptions.
explicit WebVttMuxer(const MuxerOptions& options);
~WebVttMuxer() override;
private:
// Muxer implementation overrides.
Status InitializeMuxer() override;
Status Finalize() override;
Status AddTextSample(size_t stream_id, const TextSample& sample) override;
Status FinalizeSegment(size_t stream_id,
const SegmentInfo& segment_info) override;
Status WriteToFile(const std::string& filename, uint64_t* size);
DISALLOW_COPY_AND_ASSIGN(WebVttMuxer);
std::unique_ptr<WebVttFileBuffer> buffer_;
uint64_t total_duration_ms_ = 0;
uint64_t last_cue_ms_ = 0;
uint32_t segment_index_ = 0;
};
} // namespace webvtt
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_

View File

@ -12,12 +12,15 @@
#include "packager/media/base/text_stream_info.h" #include "packager/media/base/text_stream_info.h"
#include "packager/media/event/combined_muxer_listener.h" #include "packager/media/event/combined_muxer_listener.h"
#include "packager/media/event/mock_muxer_listener.h" #include "packager/media/event/mock_muxer_listener.h"
#include "packager/media/formats/webvtt/webvtt_text_output_handler.h" #include "packager/media/formats/webvtt/webvtt_muxer.h"
#include "packager/status_test_util.h" #include "packager/status_test_util.h"
namespace shaka { namespace shaka {
namespace media { namespace media {
namespace webvtt {
namespace { namespace {
using testing::_; using testing::_;
const size_t kInputCount = 1; const size_t kInputCount = 1;
@ -38,7 +41,7 @@ const uint64_t kSegmentDuration = 10000;
const float kMillisecondsPerSecond = 1000.0f; const float kMillisecondsPerSecond = 1000.0f;
} // namespace } // namespace
class WebVttSegmentedOutputTest : public MediaHandlerTestBase { class WebVttMuxerTest : public MediaHandlerTestBase {
protected: protected:
void SetUp() { void SetUp() {
MuxerOptions muxer_options; MuxerOptions muxer_options;
@ -49,17 +52,17 @@ class WebVttSegmentedOutputTest : public MediaHandlerTestBase {
std::unique_ptr<MockMuxerListener> muxer_listener(new MockMuxerListener); std::unique_ptr<MockMuxerListener> muxer_listener(new MockMuxerListener);
muxer_listener_ = muxer_listener.get(); muxer_listener_ = muxer_listener.get();
out_ = std::make_shared<WebVttTextOutputHandler>(muxer_options, out_ = std::make_shared<WebVttMuxer>(muxer_options);
std::move(muxer_listener)); out_->SetMuxerListener(std::move(muxer_listener));
ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount)); ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount));
} }
MockMuxerListener* muxer_listener_ = nullptr; MockMuxerListener* muxer_listener_ = nullptr;
std::shared_ptr<WebVttTextOutputHandler> out_; std::shared_ptr<WebVttMuxer> out_;
}; };
TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) { TEST_F(WebVttMuxerTest, WithNoSegmentAndWithNoSamples) {
EXPECT_CALL(*muxer_listener_, OnNewSegment(_, _, _, _)).Times(0); EXPECT_CALL(*muxer_listener_, OnNewSegment(_, _, _, _)).Times(0);
{ {
@ -67,10 +70,7 @@ TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
testing::InSequence s; testing::InSequence s;
EXPECT_CALL(*muxer_listener_, OnMediaStart(_, _, _, _)); EXPECT_CALL(*muxer_listener_, OnMediaStart(_, _, _, _));
EXPECT_CALL(*muxer_listener_, OnMediaEndMock(_, _, _, _, _, _, _, _, _));
const float kMediaDuration = 0 * kSegmentDuration / kMillisecondsPerSecond;
EXPECT_CALL(*muxer_listener_,
OnMediaEndMock(_, _, _, _, _, _, _, _, kMediaDuration));
} }
ASSERT_OK(Input(kInputIndex) ASSERT_OK(Input(kInputIndex)
@ -79,7 +79,7 @@ TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
} }
TEST_F(WebVttSegmentedOutputTest, WithOneSegmentAndWithOneSample) { TEST_F(WebVttMuxerTest, WithOneSegmentAndWithOneSample) {
const char* kExpectedOutput = const char* kExpectedOutput =
"WEBVTT\n" "WEBVTT\n"
"\n" "\n"
@ -118,7 +118,7 @@ TEST_F(WebVttSegmentedOutputTest, WithOneSegmentAndWithOneSample) {
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput); ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput);
} }
TEST_F(WebVttSegmentedOutputTest, WithTwoSegmentAndWithOneSample) { TEST_F(WebVttMuxerTest, WithTwoSegmentAndWithOneSample) {
const char* kExpectedOutput1 = const char* kExpectedOutput1 =
"WEBVTT\n" "WEBVTT\n"
"\n" "\n"
@ -181,7 +181,7 @@ TEST_F(WebVttSegmentedOutputTest, WithTwoSegmentAndWithOneSample) {
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2); ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
} }
TEST_F(WebVttSegmentedOutputTest, WithAnEmptySegment) { TEST_F(WebVttMuxerTest, WithAnEmptySegment) {
const char* kExpectedOutput1 = const char* kExpectedOutput1 =
"WEBVTT\n" "WEBVTT\n"
"\n"; "\n";
@ -235,5 +235,7 @@ TEST_F(WebVttSegmentedOutputTest, WithAnEmptySegment) {
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput1); ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput1);
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2); ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
} }
} // namespace webvtt
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -1,134 +0,0 @@
// Copyright 2017 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/media/formats/webvtt/webvtt_text_output_handler.h"
#include <algorithm> // needed for min and max
#include "packager/base/logging.h"
#include "packager/file/file.h"
#include "packager/file/file_closer.h"
#include "packager/media/base/muxer_util.h"
#include "packager/status_macros.h"
namespace shaka {
namespace media {
namespace {
double kMillisecondsToSeconds = 1000.0;
std::string ToString(const std::vector<uint8_t>& v) {
return std::string(v.begin(), v.end());
}
} // namespace
WebVttTextOutputHandler::WebVttTextOutputHandler(
const MuxerOptions& muxer_options,
std::unique_ptr<MuxerListener> muxer_listener)
: muxer_options_(muxer_options),
muxer_listener_(std::move(muxer_listener)) {}
Status WebVttTextOutputHandler::InitializeInternal() {
return Status::OK;
}
Status WebVttTextOutputHandler::Process(
std::unique_ptr<StreamData> stream_data) {
switch (stream_data->stream_data_type) {
case StreamDataType::kStreamInfo:
return OnStreamInfo(*stream_data->stream_info);
case StreamDataType::kSegmentInfo:
return OnSegmentInfo(*stream_data->segment_info);
case StreamDataType::kCueEvent:
return OnCueEvent(*stream_data->cue_event);
case StreamDataType::kTextSample:
OnTextSample(*stream_data->text_sample);
return Status::OK;
default:
return Status(error::INTERNAL_ERROR,
"Invalid stream data type for this handler");
}
}
Status WebVttTextOutputHandler::OnFlushRequest(size_t input_stream_index) {
if (!buffer_) {
LOG(INFO) << "Skip stream '" << muxer_options_.segment_template
<< "' which does not contain any sample.";
return Status::OK;
}
DCHECK_EQ(buffer_->sample_count(), 0u)
<< "There should have been a segment info before flushing that would "
"have cleared out all the samples.";
const float duration_ms = static_cast<float>(total_duration_ms_);
const float duration_seconds = duration_ms / 1000.0f;
MuxerListener::MediaRanges empty_ranges;
muxer_listener_->OnMediaEnd(empty_ranges, duration_seconds);
return Status::OK;
}
Status WebVttTextOutputHandler::OnStreamInfo(const StreamInfo& info) {
buffer_.reset(
new WebVttFileBuffer(muxer_options_.transport_stream_timestamp_offset_ms,
ToString(info.codec_config())));
muxer_listener_->OnMediaStart(muxer_options_, info, info.time_scale(),
MuxerListener::kContainerText);
return Status::OK;
}
Status WebVttTextOutputHandler::OnSegmentInfo(const SegmentInfo& info) {
total_duration_ms_ += info.duration;
const std::string& segment_template = muxer_options_.segment_template;
const uint32_t index = segment_index_++;
const uint64_t start = info.start_timestamp;
const uint64_t duration = info.duration;
const uint32_t bandwidth = muxer_options_.bandwidth;
const std::string filename =
GetSegmentName(segment_template, start, index, bandwidth);
// Write everything to the file before telling the manifest so that the
// file will exist on disk.
std::unique_ptr<File, FileCloser> file(File::Open(filename.c_str(), "w"));
if (!file) {
return Status(error::FILE_FAILURE, "Failed to open " + filename);
}
buffer_->WriteTo(file.get());
buffer_->Reset();
if (!file.release()->Close()) {
return Status(error::FILE_FAILURE, "Failed to close " + filename);
}
// Update the manifest with our new file.
const uint64_t size = File::GetFileSize(filename.c_str());
muxer_listener_->OnNewSegment(filename, start, duration, size);
return Status::OK;
}
Status WebVttTextOutputHandler::OnCueEvent(const CueEvent& event) {
double timestamp_seconds = event.time_in_seconds;
double timestamp_ms = timestamp_seconds * kMillisecondsToSeconds;
uint64_t timestamp = static_cast<uint64_t>(timestamp_ms);
muxer_listener_->OnCueEvent(timestamp, event.cue_data);
return Status::OK;
}
void WebVttTextOutputHandler::OnTextSample(const TextSample& sample) {
// Skip empty samples. It is normal to see empty samples as earlier in the
// pipeline we pad the stream to remove gaps.
if (sample.payload().size()) {
buffer_->Append(sample);
}
}
} // namespace media
} // namespace shaka

View File

@ -1,58 +0,0 @@
// Copyright 2017 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_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
#include <stdint.h>
#include <vector>
#include "packager/media/base/media_handler.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/event/muxer_listener.h"
#include "packager/media/formats/webvtt/webvtt_file_buffer.h"
namespace shaka {
namespace media {
class WebVttTextOutputHandler : public MediaHandler {
public:
WebVttTextOutputHandler(const MuxerOptions& muxer_options,
std::unique_ptr<MuxerListener> muxer_listener);
virtual ~WebVttTextOutputHandler() = default;
private:
WebVttTextOutputHandler(const WebVttTextOutputHandler&) = delete;
WebVttTextOutputHandler& operator=(const WebVttTextOutputHandler&) = delete;
Status InitializeInternal() override;
Status Process(std::unique_ptr<StreamData> stream_data) override;
Status OnFlushRequest(size_t input_stream_index) override;
Status OnStreamInfo(const StreamInfo& info);
Status OnSegmentInfo(const SegmentInfo& info);
Status OnCueEvent(const CueEvent& event);
void OnTextSample(const TextSample& sample);
Status OnSegmentEnded();
void GoToNextSegment(uint64_t start_time_ms);
const MuxerOptions muxer_options_;
std::unique_ptr<MuxerListener> muxer_listener_;
// Sum together all segment durations so we know how long the stream is.
uint64_t total_duration_ms_ = 0;
uint32_t segment_index_ = 0;
std::unique_ptr<WebVttFileBuffer> buffer_;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_

View File

@ -367,9 +367,9 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
return false; return false;
} }
const bool need_segment_base = media_info.has_index_range() || const bool need_segment_base =
media_info.has_init_range() || media_info.has_index_range() || media_info.has_init_range() ||
media_info.has_reference_time_scale(); (media_info.has_reference_time_scale() && !media_info.has_text_info());
if (need_segment_base) { if (need_segment_base) {
XmlNode segment_base("SegmentBase"); XmlNode segment_base("SegmentBase");
@ -438,12 +438,12 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
segment_infos.front().duration); segment_infos.front().duration);
if (FLAGS_dash_add_last_segment_number_when_needed) { if (FLAGS_dash_add_last_segment_number_when_needed) {
uint32_t last_segment_number = start_number - 1; uint32_t last_segment_number = start_number - 1;
for (const auto& segment_info_element : segment_infos) for (const auto& segment_info_element : segment_infos)
last_segment_number += segment_info_element.repeat + 1; last_segment_number += segment_info_element.repeat + 1;
AddSupplementalProperty( AddSupplementalProperty(
"http://dashif.org/guidelines/last-segment-number", "http://dashif.org/guidelines/last-segment-number",
std::to_string(last_segment_number)); std::to_string(last_segment_number));
} }
} else { } else {
XmlNode segment_timeline("SegmentTimeline"); XmlNode segment_timeline("SegmentTimeline");

View File

@ -40,7 +40,6 @@
#include "packager/media/event/muxer_listener_factory.h" #include "packager/media/event/muxer_listener_factory.h"
#include "packager/media/event/vod_media_info_dump_muxer_listener.h" #include "packager/media/event/vod_media_info_dump_muxer_listener.h"
#include "packager/media/formats/webvtt/text_padder.h" #include "packager/media/formats/webvtt/text_padder.h"
#include "packager/media/formats/webvtt/webvtt_text_output_handler.h"
#include "packager/media/formats/webvtt/webvtt_to_mp4_handler.h" #include "packager/media/formats/webvtt/webvtt_to_mp4_handler.h"
#include "packager/media/replicator/replicator.h" #include "packager/media/replicator/replicator.h"
#include "packager/media/trick_play/trick_play_handler.h" #include "packager/media/trick_play/trick_play_handler.h"
@ -66,21 +65,6 @@ const char kMediaInfoSuffix[] = ".media_info";
const int64_t kDefaultTextZeroBiasMs = 10 * 60 * 1000; // 10 minutes const int64_t kDefaultTextZeroBiasMs = 10 * 60 * 1000; // 10 minutes
MuxerOptions CreateMuxerOptions(const StreamDescriptor& stream,
const PackagingParams& params) {
MuxerOptions options;
options.mp4_params = params.mp4_output_params;
options.transport_stream_timestamp_offset_ms =
params.transport_stream_timestamp_offset_ms;
options.temp_dir = params.temp_dir;
options.bandwidth = stream.bandwidth;
options.output_file_name = stream.output;
options.segment_template = stream.segment_template;
return options;
}
MuxerListenerFactory::StreamData ToMuxerListenerData( MuxerListenerFactory::StreamData ToMuxerListenerData(
const StreamDescriptor& stream) { const StreamDescriptor& stream) {
MuxerListenerFactory::StreamData data; MuxerListenerFactory::StreamData data;
@ -486,99 +470,30 @@ std::unique_ptr<MediaHandler> CreateTextChunker(
new TextChunker(segment_length_in_seconds)); new TextChunker(segment_length_in_seconds));
} }
Status CreateHlsTextJob(const StreamDescriptor& stream, Status CreateTtmlJobs(
const PackagingParams& packaging_params,
std::unique_ptr<MuxerListener> muxer_listener,
SyncPointQueue* sync_points,
JobManager* job_manager) {
DCHECK(muxer_listener);
DCHECK(job_manager);
if (stream.segment_template.empty()) {
return Status(error::INVALID_ARGUMENT,
"Cannot output text (" + stream.input +
") to HLS with no segment template");
}
// Text files are usually small and since the input is one file;
// there's no way for the player to do ranged requests. So set this
// value to something reasonable if it is missing.
MuxerOptions muxer_options = CreateMuxerOptions(stream, packaging_params);
muxer_options.bandwidth = stream.bandwidth ? stream.bandwidth : 256;
auto output = std::make_shared<WebVttTextOutputHandler>(
muxer_options, std::move(muxer_listener));
std::shared_ptr<Demuxer> demuxer;
RETURN_IF_ERROR(CreateDemuxer(stream, packaging_params, &demuxer));
if (!stream.language.empty())
demuxer->SetLanguageOverride(stream.stream_selector, stream.language);
auto padder = std::make_shared<TextPadder>(kDefaultTextZeroBiasMs);
RETURN_IF_ERROR(demuxer->SetHandler(stream.stream_selector, padder));
auto cue_aligner = sync_points
? std::make_shared<CueAlignmentHandler>(sync_points)
: nullptr;
auto chunker = CreateTextChunker(packaging_params.chunking_params);
job_manager->Add("Segmented Text Job", demuxer);
return MediaHandler::Chain({std::move(padder), std::move(cue_aligner),
std::move(chunker), std::move(output)});
}
Status CreateTextJobs(
const std::vector<std::reference_wrapper<const StreamDescriptor>>& streams, const std::vector<std::reference_wrapper<const StreamDescriptor>>& streams,
const PackagingParams& packaging_params, const PackagingParams& packaging_params,
SyncPointQueue* sync_points, SyncPointQueue* sync_points,
MuxerListenerFactory* muxer_listener_factory,
MuxerFactory* muxer_factory, MuxerFactory* muxer_factory,
MpdNotifier* mpd_notifier, MpdNotifier* mpd_notifier,
JobManager* job_manager) { JobManager* job_manager) {
DCHECK(muxer_listener_factory);
DCHECK(job_manager); DCHECK(job_manager);
for (const StreamDescriptor& stream : streams) { for (const StreamDescriptor& stream : streams) {
// There are currently options:
// TEXT TTML --> TEXT TTML [ supported ], for DASH only.
// TEXT WEBVTT --> TEXT WEBVTT [ supported ]
// TEXT WEBVTT --> MP4 WEBVTT [ supported ]
// MP4 WEBVTT --> MP4 WEBVTT [ unsupported ]
// MP4 WEBVTT --> TEXT WEBVTT [ unsupported ]
const auto input_container = DetermineContainerFromFileName(stream.input);
if (input_container != CONTAINER_WEBVTT &&
input_container != CONTAINER_TTML) {
return Status(error::INVALID_ARGUMENT,
"Text output format is not support for " + stream.input);
}
std::unique_ptr<MuxerListener> hls_listener =
muxer_listener_factory->CreateHlsListener(ToMuxerListenerData(stream));
// Check input to ensure that output is possible. // Check input to ensure that output is possible.
if (hls_listener && !stream.dash_only) { if (!packaging_params.hls_params.master_playlist_output.empty() &&
if (input_container == CONTAINER_TTML) { !stream.dash_only) {
return Status(error::INVALID_ARGUMENT,
"HLS does not support TTML in xml format.");
}
if (stream.segment_template.empty() || !stream.output.empty()) {
return Status(error::INVALID_ARGUMENT,
"segment_template needs to be specified for HLS text "
"output. Single file output is not supported yet.");
}
}
if (mpd_notifier && !stream.segment_template.empty() && !stream.hls_only) {
return Status(error::INVALID_ARGUMENT, return Status(error::INVALID_ARGUMENT,
"Cannot create text output for MPD with segment output."); "HLS does not support TTML in xml format.");
} }
// If we are outputting to HLS, then create the HLS test pipeline that if (!stream.segment_template.empty()) {
// will create segmented text output. return Status(error::INVALID_ARGUMENT,
if (hls_listener && !stream.dash_only) { "Segmented TTML is not supported.");
RETURN_IF_ERROR(CreateHlsTextJob(stream, packaging_params, }
std::move(hls_listener), sync_points,
job_manager)); if (GetOutputFormat(stream) != CONTAINER_TTML) {
return Status(error::INVALID_ARGUMENT,
"Converting TTML to other formats is not supported");
} }
if (!stream.output.empty()) { if (!stream.output.empty()) {
@ -694,10 +609,7 @@ Status CreateAudioVideoJobs(
if (sync_points) { if (sync_points) {
handlers.emplace_back(cue_aligner); handlers.emplace_back(cue_aligner);
} }
if (is_text) { if (!is_text) {
handlers.emplace_back(
CreateTextChunker(packaging_params.chunking_params));
} else {
handlers.emplace_back(std::make_shared<ChunkingHandler>( handlers.emplace_back(std::make_shared<ChunkingHandler>(
packaging_params.chunking_params)); packaging_params.chunking_params));
} }
@ -731,6 +643,12 @@ Status CreateAudioVideoJobs(
? std::make_shared<TrickPlayHandler>(stream.trick_play_factor) ? std::make_shared<TrickPlayHandler>(stream.trick_play_factor)
: nullptr; : nullptr;
std::shared_ptr<MediaHandler> chunker =
is_text && (!stream.segment_template.empty() ||
output_format == CONTAINER_MOV)
? CreateTextChunker(packaging_params.chunking_params)
: nullptr;
// TODO(modmaker): Move to MOV muxer? // TODO(modmaker): Move to MOV muxer?
const auto input_container = DetermineContainerFromFileName(stream.input); const auto input_container = DetermineContainerFromFileName(stream.input);
auto text_to_mp4 = auto text_to_mp4 =
@ -738,8 +656,8 @@ Status CreateAudioVideoJobs(
? std::make_shared<WebVttToMp4Handler>() ? std::make_shared<WebVttToMp4Handler>()
: nullptr; : nullptr;
RETURN_IF_ERROR( RETURN_IF_ERROR(MediaHandler::Chain(
MediaHandler::Chain({replicator, trick_play, text_to_mp4, muxer})); {replicator, trick_play, chunker, text_to_mp4, muxer}));
} }
return Status::OK; return Status::OK;
@ -758,7 +676,7 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
DCHECK(job_manager); DCHECK(job_manager);
// Group all streams based on which pipeline they will use. // Group all streams based on which pipeline they will use.
std::vector<std::reference_wrapper<const StreamDescriptor>> text_streams; std::vector<std::reference_wrapper<const StreamDescriptor>> ttml_streams;
std::vector<std::reference_wrapper<const StreamDescriptor>> std::vector<std::reference_wrapper<const StreamDescriptor>>
audio_video_streams; audio_video_streams;
@ -766,15 +684,12 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
bool has_non_transport_audio_video_streams = false; bool has_non_transport_audio_video_streams = false;
for (const StreamDescriptor& stream : stream_descriptors) { for (const StreamDescriptor& stream : stream_descriptors) {
// TODO: Find a better way to determine what stream type a stream const auto input_container = DetermineContainerFromFileName(stream.input);
// descriptor is as |stream_selector| may use an index. This would
// also allow us to use a simpler audio pipeline.
const auto output_format = GetOutputFormat(stream); const auto output_format = GetOutputFormat(stream);
if (stream.stream_selector == "text" && output_format != CONTAINER_MOV) { if (input_container == CONTAINER_TTML) {
text_streams.push_back(stream); ttml_streams.push_back(stream);
} else { } else {
audio_video_streams.push_back(stream); audio_video_streams.push_back(stream);
switch (output_format) { switch (output_format) {
case CONTAINER_MPEG2TS: case CONTAINER_MPEG2TS:
case CONTAINER_AAC: case CONTAINER_AAC:
@ -783,6 +698,9 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
case CONTAINER_EAC3: case CONTAINER_EAC3:
has_transport_audio_video_streams = true; has_transport_audio_video_streams = true;
break; break;
case CONTAINER_TTML:
case CONTAINER_WEBVTT:
break;
default: default:
has_non_transport_audio_video_streams = true; has_non_transport_audio_video_streams = true;
break; break;
@ -795,26 +713,21 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
std::sort(audio_video_streams.begin(), audio_video_streams.end(), std::sort(audio_video_streams.begin(), audio_video_streams.end(),
media::StreamDescriptorCompareFn); media::StreamDescriptorCompareFn);
if (!text_streams.empty()) { if (packaging_params.transport_stream_timestamp_offset_ms > 0) {
PackagingParams text_packaging_params = packaging_params; if (has_transport_audio_video_streams &&
if (text_packaging_params.transport_stream_timestamp_offset_ms > 0) { has_non_transport_audio_video_streams) {
if (has_transport_audio_video_streams && LOG(WARNING) << "There may be problems mixing transport streams and "
has_non_transport_audio_video_streams) { "non-transport streams. For example, the subtitles may "
LOG(WARNING) << "There may be problems mixing transport streams and " "be out of sync with non-transport streams.";
"non-transport streams. For example, the subtitles may " } else if (has_non_transport_audio_video_streams) {
"be out of sync with non-transport streams."; // Don't insert the X-TIMESTAMP-MAP in WebVTT if there is no transport
} else if (has_non_transport_audio_video_streams) { // stream.
// Don't insert the X-TIMESTAMP-MAP in WebVTT if there is no transport muxer_factory->SetTsStreamOffset(0);
// stream.
text_packaging_params.transport_stream_timestamp_offset_ms = 0;
}
} }
RETURN_IF_ERROR(CreateTextJobs(text_streams, text_packaging_params,
sync_points, muxer_listener_factory,
muxer_factory, mpd_notifier, job_manager));
} }
RETURN_IF_ERROR(CreateTtmlJobs(ttml_streams, packaging_params, sync_points,
muxer_factory, mpd_notifier, job_manager));
RETURN_IF_ERROR(CreateAudioVideoJobs( RETURN_IF_ERROR(CreateAudioVideoJobs(
audio_video_streams, packaging_params, encryption_key_source, sync_points, audio_video_streams, packaging_params, encryption_key_source, sync_points,
muxer_listener_factory, muxer_factory, job_manager)); muxer_listener_factory, muxer_factory, job_manager));