diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 5c7f53dd57..ab986b2630 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -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(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(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 vod_media_info_dump_muxer_listener( new VodMediaInfoDumpMuxerListener(output_media_info_file_name)); diff --git a/packager/app/packager_util.h b/packager/app/packager_util.h index fa7ff0ecf6..4440c38923 100644 --- a/packager/app/packager_util.h +++ b/packager/app/packager_util.h @@ -24,7 +24,6 @@ struct MpdOptions; namespace media { class KeySource; -class MediaInfo; class MediaStream; class Muxer; struct MuxerOptions; diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 479b1fd820..f6b71ff714 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -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,7 +226,8 @@ class PackagerAppTest(unittest.TestCase): def _GetStreams(self, stream_descriptors, live = False, test_file = 'bear-640x360.mp4'): streams = [] - self.output = [] + if not self.output: + self.output = [] test_data_dir = os.path.join( test_env.SRC_DIR, 'packager', 'media', 'test', 'data') @@ -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, @@ -276,14 +303,14 @@ class PackagerAppTest(unittest.TestCase): print "Updating golden file: ", golden_file_name shutil.copyfile(test_output, golden_file) else: - match = filecmp.cmp(test_output, golden_file) - if not match: - p = subprocess.Popen(['git', '--no-pager', 'diff', '--color=auto', - '--no-ext-diff', '--no-index', - golden_file, test_output], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - self.fail(output + error) + match = filecmp.cmp(test_output, golden_file) + if not match: + p = subprocess.Popen(['git', '--no-pager', 'diff', '--color=auto', + '--no-ext-diff', '--no-index', + golden_file, test_output], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + self.fail(output + error) # '*.media_info' outputs contain media file names, which is changing for # every test run. These needs to be replaced for comparison. diff --git a/packager/app/test/testdata/bear-640x360-avt-golden.mpd b/packager/app/test/testdata/bear-640x360-avt-golden.mpd new file mode 100644 index 0000000000..81e9c0b82c --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-avt-golden.mpd @@ -0,0 +1,27 @@ + + + + + + output_text.vtt + + + + + output_video.mp4 + + + + + + + + + output_audio.mp4 + + + + + + + diff --git a/packager/app/test/testdata/subtitle-english-golden.vtt b/packager/app/test/testdata/subtitle-english-golden.vtt new file mode 100644 index 0000000000..0ff3492c30 --- /dev/null +++ b/packager/app/test/testdata/subtitle-english-golden.vtt @@ -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. diff --git a/packager/app/test/testdata/subtitle-english-vtt-golden.mpd b/packager/app/test/testdata/subtitle-english-vtt-golden.mpd new file mode 100644 index 0000000000..69b6177392 --- /dev/null +++ b/packager/app/test/testdata/subtitle-english-vtt-golden.mpd @@ -0,0 +1,10 @@ + + + + + + output_text.vtt + + + + diff --git a/packager/media/base/container_names.cc b/packager/media/base/container_names.cc index 7f108edec2..cbd7c3fc74 100644 --- a/packager/media/base/container_names.cc +++ b/packager/media/base/container_names.cc @@ -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(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, + "") || + StartsWith(buffer, buffer_size, + ""); +} +} // 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; } diff --git a/packager/media/base/container_names.h b/packager/media/base/container_names.h index 24db2c5566..7239fe1f59 100644 --- a/packager/media/base/container_names.h +++ b/packager/media/base/container_names.h @@ -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 }; diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.cc b/packager/media/event/vod_media_info_dump_muxer_listener.cc index fffeff6728..f16f829fd6 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener.cc @@ -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; diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.h b/packager/media/event/vod_media_info_dump_muxer_listener.h index 8f626964c3..79a272b37c 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.h +++ b/packager/media/event/vod_media_info_dump_muxer_listener.h @@ -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_; diff --git a/packager/media/file/file.cc b/packager/media/file/file.cc index d61f1e4a3c..f495e70ec1 100644 --- a/packager/media/file/file.cc +++ b/packager/media/file/file.cc @@ -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 + 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 diff --git a/packager/media/file/file.h b/packager/media/file/file.h index 7d58f624ec..9c4489061b 100644 --- a/packager/media/file/file.h +++ b/packager/media/file/file.h @@ -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) diff --git a/packager/media/file/file_unittest.cc b/packager/media/file/file_unittest.cc index 1758971ae9..6a2479bcd4 100644 --- a/packager/media/file/file_unittest.cc +++ b/packager/media/file/file_unittest.cc @@ -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"); diff --git a/packager/media/test/data/subtitle-english.vtt b/packager/media/test/data/subtitle-english.vtt new file mode 100644 index 0000000000..0ff3492c30 --- /dev/null +++ b/packager/media/test/data/subtitle-english.vtt @@ -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.