VOD text support

Change-Id: Ic69169c31d0a54de4895d49ff8b4a758039733a7
This commit is contained in:
Rintaro Kuroiwa 2015-11-04 17:49:35 -08:00
parent 4c10755d40
commit 29e14a3d6b
14 changed files with 446 additions and 24 deletions

View File

@ -22,14 +22,17 @@
#include "packager/base/strings/stringprintf.h"
#include "packager/base/threading/simple_thread.h"
#include "packager/base/time/clock.h"
#include "packager/media/base/container_names.h"
#include "packager/media/base/demuxer.h"
#include "packager/media/base/key_source.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/base/muxer_util.h"
#include "packager/media/event/mpd_notify_muxer_listener.h"
#include "packager/media/event/vod_media_info_dump_muxer_listener.h"
#include "packager/media/file/file.h"
#include "packager/media/formats/mp4/mp4_muxer.h"
#include "packager/mpd/base/dash_iop_mpd_notifier.h"
#include "packager/mpd/base/media_info.pb.h"
#include "packager/mpd/base/mpd_builder.h"
#include "packager/mpd/base/simple_mpd_notifier.h"
@ -64,6 +67,8 @@ const char kUsage[] =
"language tag. If specified, this value overrides any language metadata "
"in the input track.\n";
const char kMediaInfoSuffix[] = ".media_info";
enum ExitStatus {
kSuccess = 0,
kNoArgument,
@ -71,6 +76,29 @@ enum ExitStatus {
kPackagingFailed,
kInternalError,
};
// TODO(rkuroiwa): Write TTML and WebVTT parser (demuxing) for a better check
// and for supporting live/segmenting (muxing). With a demuxer and a muxer,
// CreateRemuxJobs() shouldn't treat text as a special case.
std::string DetermineTextFileFormat(const std::string& file) {
std::string content;
if (!edash_packager::media::File::ReadFileToString(file.c_str(), &content)) {
LOG(ERROR) << "Failed to open file " << file
<< " to determine file format.";
return "";
}
edash_packager::media::MediaContainerName container_name =
edash_packager::media::DetermineContainer(
reinterpret_cast<const uint8_t*>(content.data()), content.size());
if (container_name == edash_packager::media::CONTAINER_WEBVTT) {
return "vtt";
} else if (container_name == edash_packager::media::CONTAINER_TTML) {
return "ttml";
}
return "";
}
} // namespace
namespace edash_packager {
@ -114,6 +142,45 @@ class RemuxJob : public base::SimpleThread {
DISALLOW_COPY_AND_ASSIGN(RemuxJob);
};
bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor,
const MuxerOptions& stream_muxer_options,
MediaInfo* text_media_info) {
const std::string& language = stream_descriptor.language;
std::string format = DetermineTextFileFormat(stream_descriptor.input);
if (format.empty()) {
LOG(ERROR) << "Failed to determine the text file format for "
<< stream_descriptor.input;
return false;
}
if (!File::Copy(stream_descriptor.input.c_str(),
stream_muxer_options.output_file_name.c_str())) {
LOG(ERROR) << "Failed to copy the input file (" << stream_descriptor.input
<< ") to output file (" << stream_muxer_options.output_file_name
<< ").";
return false;
}
text_media_info->set_media_file_name(stream_muxer_options.output_file_name);
text_media_info->set_container_type(MediaInfo::CONTAINER_TEXT);
if (stream_muxer_options.bandwidth != 0) {
text_media_info->set_bandwidth(stream_muxer_options.bandwidth);
} else {
// 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.
text_media_info->set_bandwidth(256);
}
MediaInfo::TextInfo* text_info = text_media_info->mutable_text_info();
text_info->set_format(format);
if (!language.empty())
text_info->set_language(language);
return true;
}
bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
const MuxerOptions& muxer_options,
FakeClock* fake_clock,
@ -140,6 +207,34 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
}
stream_muxer_options.bandwidth = stream_iter->bandwidth;
// Handle text input.
if (stream_iter->stream_selector == "text") {
MediaInfo text_media_info;
if (!StreamInfoToTextMediaInfo(*stream_iter, stream_muxer_options,
&text_media_info)) {
return false;
}
if (mpd_notifier) {
uint32 unused;
if (!mpd_notifier->NotifyNewContainer(text_media_info, &unused)) {
LOG(ERROR) << "Failed to process text file " << stream_iter->input;
} else {
mpd_notifier->Flush();
}
} else if (FLAGS_output_media_info) {
VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile(
text_media_info,
stream_muxer_options.output_file_name + kMediaInfoSuffix);
} else {
NOTIMPLEMENTED()
<< "--mpd_output or --output_media_info flags are "
"required for text output. Skipping manifest related output for "
<< stream_iter->input;
}
continue;
}
if (stream_iter->input != previous_input) {
// New remux job needed. Create demux and job thread.
scoped_ptr<Demuxer> demuxer(new Demuxer(stream_iter->input));
@ -180,7 +275,7 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
DCHECK(!(FLAGS_output_media_info && mpd_notifier));
if (FLAGS_output_media_info) {
const std::string output_media_info_file_name =
stream_muxer_options.output_file_name + ".media_info";
stream_muxer_options.output_file_name + kMediaInfoSuffix;
scoped_ptr<VodMediaInfoDumpMuxerListener>
vod_media_info_dump_muxer_listener(
new VodMediaInfoDumpMuxerListener(output_media_info_file_name));

View File

@ -24,7 +24,6 @@ struct MpdOptions;
namespace media {
class KeySource;
class MediaInfo;
class MediaStream;
class Muxer;
struct MuxerOptions;

View File

@ -28,6 +28,7 @@ class PackagerAppTest(unittest.TestCase):
self.tmp_dir = tempfile.mkdtemp()
self.output_prefix = os.path.join(self.tmp_dir, 'output')
self.mpd_output = self.output_prefix + '.mpd'
self.output = None
def tearDown(self):
shutil.rmtree(self.tmp_dir)
@ -70,13 +71,30 @@ class PackagerAppTest(unittest.TestCase):
self._DiffGold(self.output[0], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.mpd_output, 'bear-640x360-v-golden.mpd')
def testPackage(self):
self.packager.Package(self._GetStreams(['audio', 'video']),
def testPackageText(self):
self.packager.Package(self._GetStreams(['text'],
test_file='subtitle-english.vtt'),
self._GetFlags())
self._DiffGold(self.output[0], 'subtitle-english-golden.vtt')
self._DiffGold(self.mpd_output, 'subtitle-english-vtt-golden.mpd')
# Probably one of the most common scenarios is to package audio and video.
def testPackageAudioVideo(self):
self.packager.Package(self._GetStreams(['audio', 'video']), self._GetFlags())
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.mpd_output, 'bear-640x360-av-golden.mpd')
# Package all video, audio, and text.
def testPackageVideoAudioText(self):
audio_video_streams = self._GetStreams(['audio', 'video'])
text_stream = self._GetStreams(['text'], test_file='subtitle-english.vtt')
self.packager.Package(audio_video_streams + text_stream, self._GetFlags())
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.output[2], 'subtitle-english-golden.vtt')
self._DiffGold(self.mpd_output, 'bear-640x360-avt-golden.mpd')
def testPackageWithEncryption(self):
self.packager.Package(self._GetStreams(['audio', 'video']),
self._GetFlags(encryption=True))
@ -208,6 +226,7 @@ class PackagerAppTest(unittest.TestCase):
def _GetStreams(self, stream_descriptors, live = False,
test_file = 'bear-640x360.mp4'):
streams = []
if not self.output:
self.output = []
test_data_dir = os.path.join(
@ -221,12 +240,20 @@ class PackagerAppTest(unittest.TestCase):
'segment_template=%s-$Number$.m4s')
streams.append(stream % (input, stream_descriptor, output, output))
else:
output = '%s_%s.mp4' % (self.output_prefix, stream_descriptor)
output = '%s_%s.%s' % (self.output_prefix,
stream_descriptor,
self._GetExtension(stream_descriptor))
stream = 'input=%s,stream=%s,output=%s'
streams.append(stream % (input, stream_descriptor, output))
self.output.append(output)
return streams
def _GetExtension(self, stream_descriptor):
# TODO(rkuroiwa): Support ttml.
if stream_descriptor == "text":
return "vtt"
return "mp4"
def _GetFlags(self, encryption = False, random_iv = False,
widevine_encryption = False, key_rotation = False,
live = False, dash_if_iop=False, output_media_info = False,

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT2.763174533843994S">
<Period>
<AdaptationSet id="0" contentType="text">
<Representation id="0" bandwidth="256" mimeType="text/vtt">
<BaseURL>output_text.vtt</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" frameRate="30000/1001" subSegmentAlignment="true" par="16:9">
<Representation id="1" bandwidth="881637" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" width="640" height="360" frameRate="30000/1001">
<BaseURL>output_video.mp4</BaseURL>
<SegmentBase indexRange="677-744" timescale="30000">
<Initialization range="0-676"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="audio" subSegmentAlignment="true">
<Representation id="2" bandwidth="126087" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>output_audio.mp4</BaseURL>
<SegmentBase indexRange="611-678" timescale="44100">
<Initialization range="0-610"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,79 @@
WEBVTT FILE
1
00:00:03.837 --> 00:00:07.299
Captain's log, stardate 41636.9.
2
00:00:07.466 --> 00:00:11.845
As feared, our examination of the
overdue Federation freighter Odin,
3
00:00:12.012 --> 00:00:16.475
disabled by an asteroid collision,
revealed no life signs.
4
00:00:16.642 --> 00:00:19.019
However three escape pods
were missing,
5
00:00:19.186 --> 00:00:21.939
suggesting
the possibility of survivors.
6
00:00:22.606 --> 00:00:27.861
- Ready to orbit Angel One.
- What kind of place is this, Data?
7
00:00:28.028 --> 00:00:31.615
A Class-M planet supporting
carbon-based flora and fauna,
8
00:00:31.782 --> 00:00:34.326
sparsely populated
with intelligent life.
9
00:00:34.493 --> 00:00:38.497
Similar in technological development
to mid-20th century Earth.
10
00:00:38.664 --> 00:00:41.000
Kinda like being marooned at home.
11
00:00:41.166 --> 00:00:43.586
Assuming any survivors
made it this far.
12
00:00:43.794 --> 00:00:49.174
It is the closest planet, but to
go the distance we did in two days,
13
00:00:49.341 --> 00:00:52.344
would've taken the Odin escape pod
five months.
14
00:00:52.511 --> 00:00:54.680
Five months, six days, 11 hours,
two min...
15
00:00:54.847 --> 00:00:58.392
- Thank you, Data.
- ...and 57 seconds.
16
00:00:58.559 --> 00:01:01.353
Receiving an audio signal
from Angel One.

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT0S">
<Period>
<AdaptationSet id="0" contentType="text">
<Representation id="0" bandwidth="256" mimeType="text/vtt">
<BaseURL>output_text.vtt</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -1618,6 +1618,29 @@ static MediaContainerName LookupContainerByFirst4(const uint8_t* buffer,
return CONTAINER_UNKNOWN;
}
namespace {
const char kWebVtt[] = "WEBVTT";
bool CheckWebVtt(const uint8_t* buffer, int buffer_size) {
const int offset =
StartsWith(buffer, buffer_size, UTF8_BYTE_ORDER_MARK) ? 3 : 0;
return StartsWith(buffer + offset, buffer_size - offset,
reinterpret_cast<const uint8_t*>(kWebVtt),
arraysize(kWebVtt) - 1);
}
// TODO(rkuroiwa): This check is a very simple check to see if it is UTF-8 or
// UTF-16, which is not sufficient to determine whether it is TTML. Check if the
// entire buffer is a valid TTML.
bool CheckTtml(const uint8_t* buffer, int buffer_size) {
return StartsWith(buffer, buffer_size,
"<?xml version='1.0' encoding='UTF-8'?>") ||
StartsWith(buffer, buffer_size,
"<?xml version='1.0' encoding='UTF-16'?>");
}
} // namespace
// Attempt to determine the container name from the buffer provided.
MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
DCHECK(buffer);
@ -1632,6 +1655,10 @@ MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
if (result != CONTAINER_UNKNOWN)
return result;
// WebVTT check only checks for the first few bytes.
if (CheckWebVtt(buffer, buffer_size))
return CONTAINER_WEBVTT;
// Additional checks that may scan a portion of the buffer.
if (CheckMpeg2ProgramStream(buffer, buffer_size))
return CONTAINER_MPEG2PS;
@ -1666,6 +1693,11 @@ MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
return CONTAINER_EAC3;
}
// To do a TTML check, it (should) do a schema check which requires scanning
// the whole content.
if (CheckTtml(buffer, buffer_size))
return CONTAINER_TTML;
return CONTAINER_UNKNOWN;
}

View File

@ -47,9 +47,11 @@ enum MediaContainerName {
CONTAINER_RM, // RM (RealMedia)
CONTAINER_SRT, // SRT (SubRip subtitle)
CONTAINER_SWF, // SWF (ShockWave Flash)
CONTAINER_TTML, // TTML file.
CONTAINER_VC1, // VC-1
CONTAINER_WAV, // WAV / WAVE (Waveform Audio)
CONTAINER_WEBM, // Matroska / WebM
CONTAINER_WEBVTT, // WebVTT file.
CONTAINER_WTV, // WTV (Windows Television)
CONTAINER_MAX // Must be last
};

View File

@ -19,8 +19,8 @@ namespace edash_packager {
namespace media {
VodMediaInfoDumpMuxerListener::VodMediaInfoDumpMuxerListener(
const std::string& output_file_name)
: output_file_name_(output_file_name), is_encrypted_(false) {}
const std::string& output_file_path)
: output_file_name_(output_file_path), is_encrypted_(false) {}
VodMediaInfoDumpMuxerListener::~VodMediaInfoDumpMuxerListener() {}
@ -97,7 +97,7 @@ void VodMediaInfoDumpMuxerListener::OnMediaEnd(bool has_init_range,
LOG(ERROR) << "Failed to generate VOD information from input.";
return;
}
SerializeMediaInfoToFile();
WriteMediaInfoToFile(*media_info_, output_file_name_);
}
void VodMediaInfoDumpMuxerListener::OnNewSegment(uint64_t start_time,
@ -105,17 +105,20 @@ void VodMediaInfoDumpMuxerListener::OnNewSegment(uint64_t start_time,
uint64_t segment_file_size) {
}
bool VodMediaInfoDumpMuxerListener::SerializeMediaInfoToFile() {
// static
bool VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile(
const edash_packager::MediaInfo& media_info,
const std::string& output_file_path) {
std::string output_string;
if (!google::protobuf::TextFormat::PrintToString(*media_info_,
if (!google::protobuf::TextFormat::PrintToString(media_info,
&output_string)) {
LOG(ERROR) << "Failed to serialize MediaInfo to string.";
return false;
}
media::File* file = File::Open(output_file_name_.c_str(), "w");
media::File* file = File::Open(output_file_path.c_str(), "w");
if (!file) {
LOG(ERROR) << "Failed to open " << output_file_name_;
LOG(ERROR) << "Failed to open " << output_file_path;
return false;
}
if (file->Write(output_string.data(), output_string.size()) <= 0) {
@ -124,7 +127,7 @@ bool VodMediaInfoDumpMuxerListener::SerializeMediaInfoToFile() {
return false;
}
if (!file->Close()) {
LOG(ERROR) << "Failed to close " << output_file_name_;
LOG(ERROR) << "Failed to close " << output_file_path;
return false;
}
return true;

View File

@ -59,9 +59,16 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener {
uint64_t segment_file_size) override;
/// @}
/// Write @a media_info to @a output_file_path in human readable format.
/// @param media_info is the MediaInfo to write out.
/// @param output_file_path is the path of the output file.
/// @return true on success, false otherwise.
// TODO(rkuroiwa): Move this to muxer_listener_internal and rename
// muxer_listener_internal to muxer_listener_util.
static bool WriteMediaInfoToFile(const MediaInfo& media_info,
const std::string& output_file_path);
private:
// Write |media_info_| to |output_file_name_|.
bool SerializeMediaInfoToFile();
std::string output_file_name_;
std::string scheme_id_uri_;

View File

@ -175,5 +175,35 @@ bool File::ReadFileToString(const char* file_name, std::string* contents) {
return len == 0;
}
bool File::Copy(const char* from_file_name, const char* to_file_name) {
std::string content;
if (!ReadFileToString(from_file_name, &content)) {
LOG(ERROR) << "Failed to open file " << from_file_name;
return false;
}
scoped_ptr<edash_packager::media::File, edash_packager::media::FileCloser>
output_file(edash_packager::media::File::Open(to_file_name, "w"));
if (!output_file) {
LOG(ERROR) << "Failed to write to " << to_file_name;
return false;
}
uint64_t bytes_left = content.size();
uint64_t total_bytes_written = 0;
const char* content_cstr = content.c_str();
while (bytes_left > total_bytes_written) {
const int64_t bytes_written =
output_file->Write(content_cstr + total_bytes_written, bytes_left);
if (bytes_written < 0) {
LOG(ERROR) << "Failure while writing to " << to_file_name;
return false;
}
total_bytes_written += bytes_written;
}
return true;
}
} // namespace media
} // namespace edash_packager

View File

@ -102,6 +102,14 @@ class File {
/// @return true on success, false otherwise.
static bool ReadFileToString(const char* file_name, std::string* contents);
/// Copies files. This is not good for copying huge files. Although not
/// recommended, it is safe to have source file and destination file name be
/// the same.
/// @param from_file_name is the source file name.
/// @param to_file_name is the destination file name.
/// @return true on success, false otherwise.
static bool Copy(const char* from_file_name, const char* to_file_name);
protected:
explicit File(const std::string& file_name) : file_name_(file_name) {}
/// Do *not* call the destructor directly (with the "delete" keyword)

View File

@ -60,6 +60,30 @@ TEST_F(LocalFileTest, Size) {
ASSERT_EQ(kDataSize, File::GetFileSize(local_file_name_.c_str()));
}
TEST_F(LocalFileTest, Copy) {
ASSERT_EQ(kDataSize,
base::WriteFile(test_file_path_, data_.data(), kDataSize));
base::FilePath temp_dir;
ASSERT_TRUE(base::CreateNewTempDirectory("", &temp_dir));
// Copy the test file to temp dir as filename "a".
base::FilePath destination = temp_dir.Append("a");
ASSERT_TRUE(
File::Copy(local_file_name_.c_str(), destination.value().c_str()));
// Make a buffer bigger than the expected file content size to make sure that
// there isn't extra stuff appended.
char copied_file_content_buffer[kDataSize * 2] = {};
ASSERT_EQ(kDataSize, base::ReadFile(destination,
copied_file_content_buffer,
arraysize(copied_file_content_buffer)));
ASSERT_EQ(data_, std::string(copied_file_content_buffer, kDataSize));
base::DeleteFile(temp_dir, true);
}
TEST_F(LocalFileTest, Write) {
// Write file using File API.
File* file = File::Open(local_file_name_.c_str(), "w");

View File

@ -0,0 +1,79 @@
WEBVTT FILE
1
00:00:03.837 --> 00:00:07.299
Captain's log, stardate 41636.9.
2
00:00:07.466 --> 00:00:11.845
As feared, our examination of the
overdue Federation freighter Odin,
3
00:00:12.012 --> 00:00:16.475
disabled by an asteroid collision,
revealed no life signs.
4
00:00:16.642 --> 00:00:19.019
However three escape pods
were missing,
5
00:00:19.186 --> 00:00:21.939
suggesting
the possibility of survivors.
6
00:00:22.606 --> 00:00:27.861
- Ready to orbit Angel One.
- What kind of place is this, Data?
7
00:00:28.028 --> 00:00:31.615
A Class-M planet supporting
carbon-based flora and fauna,
8
00:00:31.782 --> 00:00:34.326
sparsely populated
with intelligent life.
9
00:00:34.493 --> 00:00:38.497
Similar in technological development
to mid-20th century Earth.
10
00:00:38.664 --> 00:00:41.000
Kinda like being marooned at home.
11
00:00:41.166 --> 00:00:43.586
Assuming any survivors
made it this far.
12
00:00:43.794 --> 00:00:49.174
It is the closest planet, but to
go the distance we did in two days,
13
00:00:49.341 --> 00:00:52.344
would've taken the Odin escape pod
five months.
14
00:00:52.511 --> 00:00:54.680
Five months, six days, 11 hours,
two min...
15
00:00:54.847 --> 00:00:58.392
- Thank you, Data.
- ...and 57 seconds.
16
00:00:58.559 --> 00:01:01.353
Receiving an audio signal
from Angel One.