OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "media/filters/chunk_demuxer.h" | 5 #include "media/filters/chunk_demuxer.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/logging.h" | 8 #include "base/logging.h" |
9 #include "base/message_loop.h" | 9 #include "base/message_loop.h" |
10 #include "base/string_util.h" | 10 #include "base/string_util.h" |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
66 return new mp4::MP4StreamParser(); | 66 return new mp4::MP4StreamParser(); |
67 } | 67 } |
68 | 68 |
69 static const SupportedTypeInfo kSupportedTypeInfo[] = { | 69 static const SupportedTypeInfo kSupportedTypeInfo[] = { |
70 { "video/webm", &BuildWebMParser, kVideoWebMCodecs }, | 70 { "video/webm", &BuildWebMParser, kVideoWebMCodecs }, |
71 { "audio/webm", &BuildWebMParser, kAudioWebMCodecs }, | 71 { "audio/webm", &BuildWebMParser, kAudioWebMCodecs }, |
72 { "video/mp4", &BuildMP4Parser, kVideoMP4Codecs }, | 72 { "video/mp4", &BuildMP4Parser, kVideoMP4Codecs }, |
73 { "audio/mp4", &BuildMP4Parser, kAudioMP4Codecs }, | 73 { "audio/mp4", &BuildMP4Parser, kAudioMP4Codecs }, |
74 }; | 74 }; |
75 | 75 |
76 | |
77 // The fake total size we use for converting times to bytes | |
78 // for AddBufferedByteRange() calls. | |
79 enum { kFakeTotalBytes = 1000000 }; | |
Ami GONE FROM CHROMIUM
2012/06/19 17:40:37
TODO to drop this in favor of teaching Pipeline to
acolwell GONE FROM CHROMIUM
2012/06/19 19:50:15
Done.
| |
80 | |
76 // Checks to see if the specified |type| and |codecs| list are supported. | 81 // Checks to see if the specified |type| and |codecs| list are supported. |
77 // Returns true if |type| and all codecs listed in |codecs| are supported. | 82 // Returns true if |type| and all codecs listed in |codecs| are supported. |
78 // |factory_function| contains a function that can build a StreamParser | 83 // |factory_function| contains a function that can build a StreamParser |
79 // for this type. | 84 // for this type. |
80 // |has_audio| is true if an audio codec was specified. | 85 // |has_audio| is true if an audio codec was specified. |
81 // |has_video| is true if a video codec was specified. | 86 // |has_video| is true if a video codec was specified. |
82 // Returns false otherwise. The values of |factory_function|, |has_audio|, | 87 // Returns false otherwise. The values of |factory_function|, |has_audio|, |
83 // and |has_video| are undefined. | 88 // and |has_video| are undefined. |
84 static bool IsSupported(const std::string& type, | 89 static bool IsSupported(const std::string& type, |
85 std::vector<std::string>& codecs, | 90 std::vector<std::string>& codecs, |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
140 class ChunkDemuxerStream : public DemuxerStream { | 145 class ChunkDemuxerStream : public DemuxerStream { |
141 public: | 146 public: |
142 typedef std::deque<scoped_refptr<StreamParserBuffer> > BufferQueue; | 147 typedef std::deque<scoped_refptr<StreamParserBuffer> > BufferQueue; |
143 typedef std::deque<ReadCB> ReadCBQueue; | 148 typedef std::deque<ReadCB> ReadCBQueue; |
144 typedef std::deque<base::Closure> ClosureQueue; | 149 typedef std::deque<base::Closure> ClosureQueue; |
145 | 150 |
146 explicit ChunkDemuxerStream(const AudioDecoderConfig& audio_config); | 151 explicit ChunkDemuxerStream(const AudioDecoderConfig& audio_config); |
147 explicit ChunkDemuxerStream(const VideoDecoderConfig& video_config); | 152 explicit ChunkDemuxerStream(const VideoDecoderConfig& video_config); |
148 | 153 |
149 void StartWaitingForSeek(); | 154 void StartWaitingForSeek(); |
150 void Seek(base::TimeDelta time); | 155 void Seek(base::TimeDelta time); |
Ami GONE FROM CHROMIUM
2012/06/19 17:40:37
At 21 occurrences in this file, I'd say base::Time
acolwell GONE FROM CHROMIUM
2012/06/19 19:50:15
Done.
| |
151 bool IsSeekPending() const; | 156 bool IsSeekPending() const; |
152 void Flush(); | 157 void Flush(); |
153 | 158 |
154 // Add buffers to this stream. Buffers are stored in SourceBufferStreams, | 159 // Add buffers to this stream. Buffers are stored in SourceBufferStreams, |
155 // which handle ordering and overlap resolution. | 160 // which handle ordering and overlap resolution. |
156 // Returns true if buffers were successfully added. | 161 // Returns true if buffers were successfully added. |
157 bool Append(const StreamParser::BufferQueue& buffers); | 162 bool Append(const StreamParser::BufferQueue& buffers); |
158 | 163 |
159 // Returns a list of the buffered time ranges. | 164 // Returns a list of the buffered time ranges. |
160 SourceBufferStream::TimespanList GetBufferedTime() const; | 165 Ranges<base::TimeDelta> GetBufferedTime() const; |
161 | 166 |
162 // Signal to the stream that buffers handed in through subsequent calls to | 167 // Signal to the stream that buffers handed in through subsequent calls to |
163 // Append() belong to a media segment that starts at |start_timestamp|. | 168 // Append() belong to a media segment that starts at |start_timestamp|. |
164 void OnNewMediaSegment(base::TimeDelta start_timestamp); | 169 void OnNewMediaSegment(base::TimeDelta start_timestamp); |
165 | 170 |
166 // Called when mid-stream config updates occur. | 171 // Called when mid-stream config updates occur. |
167 // Returns true if the new config is accepted. | 172 // Returns true if the new config is accepted. |
168 // Returns false if the new config should trigger an error. | 173 // Returns false if the new config should trigger an error. |
169 bool UpdateAudioConfig(const AudioDecoderConfig& config); | 174 bool UpdateAudioConfig(const AudioDecoderConfig& config); |
170 bool UpdateVideoConfig(const VideoDecoderConfig& config); | 175 bool UpdateVideoConfig(const VideoDecoderConfig& config); |
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
283 } | 288 } |
284 CreateReadDoneClosures_Locked(&closures); | 289 CreateReadDoneClosures_Locked(&closures); |
285 } | 290 } |
286 | 291 |
287 for (ClosureQueue::iterator it = closures.begin(); it != closures.end(); ++it) | 292 for (ClosureQueue::iterator it = closures.begin(); it != closures.end(); ++it) |
288 it->Run(); | 293 it->Run(); |
289 | 294 |
290 return true; | 295 return true; |
291 } | 296 } |
292 | 297 |
293 SourceBufferStream::TimespanList ChunkDemuxerStream::GetBufferedTime() const { | 298 Ranges<base::TimeDelta> ChunkDemuxerStream::GetBufferedTime() const { |
294 base::AutoLock auto_lock(lock_); | 299 base::AutoLock auto_lock(lock_); |
295 return stream_->GetBufferedTime(); | 300 return stream_->GetBufferedTime(); |
296 } | 301 } |
297 | 302 |
298 bool ChunkDemuxerStream::UpdateAudioConfig(const AudioDecoderConfig& config) { | 303 bool ChunkDemuxerStream::UpdateAudioConfig(const AudioDecoderConfig& config) { |
299 DCHECK(config.IsValidConfig()); | 304 DCHECK(config.IsValidConfig()); |
300 DCHECK_EQ(type_, AUDIO); | 305 DCHECK_EQ(type_, AUDIO); |
301 | 306 |
302 const AudioDecoderConfig& current_config = | 307 const AudioDecoderConfig& current_config = |
303 stream_->GetCurrentAudioDecoderConfig(); | 308 stream_->GetCurrentAudioDecoderConfig(); |
(...skipping 152 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
456 if (!stream_->GetNextBuffer(&buffer)) | 461 if (!stream_->GetNextBuffer(&buffer)) |
457 return; | 462 return; |
458 closures->push_back(base::Bind(read_cbs_.front(), buffer)); | 463 closures->push_back(base::Bind(read_cbs_.front(), buffer)); |
459 read_cbs_.pop_front(); | 464 read_cbs_.pop_front(); |
460 } | 465 } |
461 } | 466 } |
462 | 467 |
463 ChunkDemuxer::ChunkDemuxer(ChunkDemuxerClient* client) | 468 ChunkDemuxer::ChunkDemuxer(ChunkDemuxerClient* client) |
464 : state_(WAITING_FOR_INIT), | 469 : state_(WAITING_FOR_INIT), |
465 host_(NULL), | 470 host_(NULL), |
466 client_(client), | 471 client_(client) { |
467 buffered_bytes_(0) { | |
468 DCHECK(client); | 472 DCHECK(client); |
469 } | 473 } |
470 | 474 |
471 void ChunkDemuxer::Initialize(DemuxerHost* host, | 475 void ChunkDemuxer::Initialize(DemuxerHost* host, |
472 const PipelineStatusCB& cb) { | 476 const PipelineStatusCB& cb) { |
473 DVLOG(1) << "Init()"; | 477 DVLOG(1) << "Init()"; |
474 { | 478 { |
475 base::AutoLock auto_lock(lock_); | 479 base::AutoLock auto_lock(lock_); |
476 DCHECK_EQ(state_, WAITING_FOR_INIT); | 480 DCHECK_EQ(state_, WAITING_FOR_INIT); |
477 host_ = host; | 481 host_ = host; |
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
620 delete stream_parser_map_[id]; | 624 delete stream_parser_map_[id]; |
621 stream_parser_map_.erase(id); | 625 stream_parser_map_.erase(id); |
622 | 626 |
623 if (source_id_audio_ == id && audio_) | 627 if (source_id_audio_ == id && audio_) |
624 audio_->Shutdown(); | 628 audio_->Shutdown(); |
625 | 629 |
626 if (source_id_video_ == id && video_) | 630 if (source_id_video_ == id && video_) |
627 video_->Shutdown(); | 631 video_->Shutdown(); |
628 } | 632 } |
629 | 633 |
630 bool ChunkDemuxer::GetBufferedRanges(const std::string& id, | 634 Ranges<base::TimeDelta> ChunkDemuxer::GetBufferedRanges( |
631 Ranges* ranges_out) const { | 635 const std::string& id) const { |
632 DCHECK(!id.empty()); | 636 DCHECK(!id.empty()); |
633 DCHECK_GT(stream_parser_map_.count(id), 0u); | 637 DCHECK_GT(stream_parser_map_.count(id), 0u); |
634 DCHECK(id == source_id_audio_ || id == source_id_video_); | 638 DCHECK(id == source_id_audio_ || id == source_id_video_); |
635 DCHECK(ranges_out); | |
636 | 639 |
637 base::AutoLock auto_lock(lock_); | 640 base::AutoLock auto_lock(lock_); |
638 | 641 |
639 if (id == source_id_audio_ && id != source_id_video_) { | 642 if (id == source_id_audio_ && id != source_id_video_) { |
640 // Only include ranges that have been buffered in |audio_| | 643 // Only include ranges that have been buffered in |audio_| |
641 return CopyIntoRanges(audio_->GetBufferedTime(), ranges_out); | 644 return audio_->GetBufferedTime(); |
642 } | 645 } |
643 | 646 |
644 if (id != source_id_audio_ && id == source_id_video_) { | 647 if (id != source_id_audio_ && id == source_id_video_) { |
645 // Only include ranges that have been buffered in |video_| | 648 // Only include ranges that have been buffered in |video_| |
646 return CopyIntoRanges(video_->GetBufferedTime(), ranges_out); | 649 return video_->GetBufferedTime(); |
647 } | 650 } |
648 | 651 |
652 return ComputeIntersection(); | |
653 } | |
654 | |
655 Ranges<base::TimeDelta> ChunkDemuxer::ComputeIntersection() const { | |
656 lock_.AssertAcquired(); | |
657 | |
658 if (!audio_ || !video_) | |
659 return Ranges<base::TimeDelta>(); | |
660 | |
649 // Include ranges that have been buffered in both |audio_| and |video_|. | 661 // Include ranges that have been buffered in both |audio_| and |video_|. |
650 SourceBufferStream::TimespanList audio_ranges = audio_->GetBufferedTime(); | 662 Ranges<base::TimeDelta> audio_ranges = audio_->GetBufferedTime(); |
651 SourceBufferStream::TimespanList video_ranges = video_->GetBufferedTime(); | 663 Ranges<base::TimeDelta> video_ranges = video_->GetBufferedTime(); |
652 SourceBufferStream::TimespanList::const_iterator video_ranges_itr = | 664 Ranges<base::TimeDelta> result = audio_ranges.IntersectionWith(video_ranges); |
653 video_ranges.begin(); | |
654 SourceBufferStream::TimespanList::const_iterator audio_ranges_itr = | |
655 audio_ranges.begin(); | |
656 bool success = false; | |
657 | 665 |
658 while (audio_ranges_itr != audio_ranges.end() && | 666 if (state_ == ENDED) { |
659 video_ranges_itr != video_ranges.end()) { | 667 // If appending has ended, extend the intersection to include all of the |
660 // If this is the last range and EndOfStream() was called (i.e. all data | 668 // data from the last ranges for each stream. This allows the buffered |
661 // has been appended), choose the max end point of the ranges. | 669 // information to match the actual time range that will get played out if |
662 bool last_range_after_ended = | 670 // the streams have slightly different lengths. We only do this if the |
663 state_ == ENDED && | 671 // final ranges overlap because this indicates there is actually a location |
664 (audio_ranges_itr + 1) == audio_ranges.end() && | 672 // you can seek to, near the end, where we have data for both streams. |
665 (video_ranges_itr + 1) == video_ranges.end(); | 673 base::TimeDelta audio_start = audio_ranges.start(audio_ranges.size() - 1); |
674 base::TimeDelta audio_end = audio_ranges.end(audio_ranges.size() - 1); | |
675 base::TimeDelta video_start = video_ranges.start(video_ranges.size() - 1); | |
676 base::TimeDelta video_end = video_ranges.end(video_ranges.size() - 1); | |
666 | 677 |
667 // Audio range start time is within the video range. | 678 if ((audio_start <= video_start && video_start <= audio_end) || |
668 if ((*audio_ranges_itr).first >= (*video_ranges_itr).first && | 679 (video_start <= audio_start && audio_start <= video_end)) { |
669 (*audio_ranges_itr).first <= (*video_ranges_itr).second) { | 680 result.Add(std::max(audio_start, video_start), |
Ami GONE FROM CHROMIUM
2012/06/19 17:40:37
should this be a "min" instead of a max? Otherwis
acolwell GONE FROM CHROMIUM
2012/06/19 19:50:15
I don't want min here. I only want the intersectio
| |
670 AddIntersectionRange(*audio_ranges_itr, *video_ranges_itr, | 681 std::max(audio_end, video_end)); |
671 last_range_after_ended, ranges_out); | |
672 audio_ranges_itr++; | |
673 success = true; | |
674 continue; | |
675 } | 682 } |
676 | |
677 // Video range start time is within the audio range. | |
678 if ((*video_ranges_itr).first >= (*audio_ranges_itr).first && | |
679 (*video_ranges_itr).first <= (*audio_ranges_itr).second) { | |
680 AddIntersectionRange(*video_ranges_itr, *audio_ranges_itr, | |
681 last_range_after_ended, ranges_out); | |
682 video_ranges_itr++; | |
683 success = true; | |
684 continue; | |
685 } | |
686 | |
687 // No overlap was found. Increment the earliest one and keep looking. | |
688 if ((*audio_ranges_itr).first < (*video_ranges_itr).first) | |
689 audio_ranges_itr++; | |
690 else | |
691 video_ranges_itr++; | |
692 } | 683 } |
693 | 684 |
694 return success; | 685 return result; |
695 } | |
696 | |
697 bool ChunkDemuxer::CopyIntoRanges( | |
698 const SourceBufferStream::TimespanList& timespans, | |
699 Ranges* ranges_out) const { | |
700 for (SourceBufferStream::TimespanList::const_iterator itr = timespans.begin(); | |
701 itr != timespans.end(); ++itr) { | |
702 ranges_out->push_back(*itr); | |
703 } | |
704 return !timespans.empty(); | |
705 } | |
706 | |
707 void ChunkDemuxer::AddIntersectionRange( | |
708 SourceBufferStream::Timespan timespan_a, | |
709 SourceBufferStream::Timespan timespan_b, | |
710 bool last_range_after_ended, | |
711 Ranges* ranges_out) const { | |
712 base::TimeDelta start = timespan_a.first; | |
713 | |
714 // If this is the last range after EndOfStream() was called, choose the later | |
715 // end point of the ranges, otherwise choose the earlier. | |
716 base::TimeDelta end; | |
717 if (last_range_after_ended) | |
718 end = std::max(timespan_a.second, timespan_b.second); | |
719 else | |
720 end = std::min(timespan_a.second, timespan_b.second); | |
721 | |
722 ranges_out->push_back(std::make_pair(start, end)); | |
723 } | 686 } |
724 | 687 |
725 bool ChunkDemuxer::AppendData(const std::string& id, | 688 bool ChunkDemuxer::AppendData(const std::string& id, |
726 const uint8* data, | 689 const uint8* data, |
727 size_t length) { | 690 size_t length) { |
728 DVLOG(1) << "AppendData(" << id << ", " << length << ")"; | 691 DVLOG(1) << "AppendData(" << id << ", " << length << ")"; |
729 | 692 |
730 DCHECK(!id.empty()); | 693 DCHECK(!id.empty()); |
731 DCHECK(data); | 694 DCHECK(data); |
732 DCHECK_GT(length, 0u); | 695 DCHECK_GT(length, 0u); |
733 | 696 |
734 int64 buffered_bytes = 0; | 697 Ranges<base::TimeDelta> ranges; |
735 | 698 |
736 PipelineStatusCB cb; | 699 PipelineStatusCB cb; |
737 { | 700 { |
738 base::AutoLock auto_lock(lock_); | 701 base::AutoLock auto_lock(lock_); |
739 | 702 |
740 // Capture if the SourceBuffer has a pending seek before we start parsing. | 703 // Capture if the SourceBuffer has a pending seek before we start parsing. |
741 bool old_seek_pending = IsSeekPending_Locked(); | 704 bool old_seek_pending = IsSeekPending_Locked(); |
742 | 705 |
743 switch (state_) { | 706 switch (state_) { |
744 case INITIALIZING: | 707 case INITIALIZING: |
(...skipping 20 matching lines...) Expand all Loading... | |
765 DVLOG(1) << "AppendData(): called in unexpected state " << state_; | 728 DVLOG(1) << "AppendData(): called in unexpected state " << state_; |
766 return false; | 729 return false; |
767 } | 730 } |
768 | 731 |
769 // Check to see if data was appended at the pending seek point. This | 732 // Check to see if data was appended at the pending seek point. This |
770 // indicates we have parsed enough data to complete the seek. | 733 // indicates we have parsed enough data to complete the seek. |
771 if (old_seek_pending && !IsSeekPending_Locked() && !seek_cb_.is_null()) { | 734 if (old_seek_pending && !IsSeekPending_Locked() && !seek_cb_.is_null()) { |
772 std::swap(cb, seek_cb_); | 735 std::swap(cb, seek_cb_); |
773 } | 736 } |
774 | 737 |
775 buffered_bytes_ += length; | 738 if (duration_ > base::TimeDelta() && duration_ != kInfiniteDuration()) { |
776 buffered_bytes = buffered_bytes_; | 739 if (audio_ && !video_) { |
740 ranges = audio_->GetBufferedTime(); | |
741 } else if (!audio_ && video_) { | |
742 ranges = video_->GetBufferedTime(); | |
743 } else { | |
744 ranges = ComputeIntersection(); | |
745 } | |
746 } | |
777 } | 747 } |
778 | 748 |
779 // Notify the host of 'network activity' because we got data, using a bogus | 749 for (size_t i = 0; i < ranges.size(); ++i) { |
780 // range. | 750 DCHECK(duration_ > base::TimeDelta()); |
Ami GONE FROM CHROMIUM
2012/06/19 17:40:37
silly to do this inside the for-loop; move up as:
acolwell GONE FROM CHROMIUM
2012/06/19 19:50:15
Done. Used !ranges.size() since empty() doesn't ex
| |
781 host_->AddBufferedByteRange(0, buffered_bytes); | 751 |
752 // Notify the host of 'network activity' because we got data. | |
753 int64 start = | |
754 kFakeTotalBytes * ranges.start(i).InSecondsF() / duration_.InSecondsF(); | |
755 int64 end = | |
756 kFakeTotalBytes * ranges.end(i).InSecondsF() / duration_.InSecondsF(); | |
757 host_->AddBufferedByteRange(start, end); | |
758 } | |
782 | 759 |
783 if (!cb.is_null()) | 760 if (!cb.is_null()) |
784 cb.Run(PIPELINE_OK); | 761 cb.Run(PIPELINE_OK); |
785 | 762 |
786 return true; | 763 return true; |
787 } | 764 } |
788 | 765 |
789 void ChunkDemuxer::Abort(const std::string& id) { | 766 void ChunkDemuxer::Abort(const std::string& id) { |
790 DVLOG(1) << "Abort(" << id << ")"; | 767 DVLOG(1) << "Abort(" << id << ")"; |
791 DCHECK(!id.empty()); | 768 DCHECK(!id.empty()); |
(...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
940 } | 917 } |
941 | 918 |
942 if (duration > duration_) | 919 if (duration > duration_) |
943 duration_ = duration; | 920 duration_ = duration; |
944 | 921 |
945 // Wait until all streams have initialized. | 922 // Wait until all streams have initialized. |
946 if ((!source_id_audio_.empty() && !audio_) || | 923 if ((!source_id_audio_.empty() && !audio_) || |
947 (!source_id_video_.empty() && !video_)) | 924 (!source_id_video_.empty() && !video_)) |
948 return; | 925 return; |
949 | 926 |
927 if (duration_ > base::TimeDelta() && duration_ != kInfiniteDuration()) | |
928 host_->SetTotalBytes(kFakeTotalBytes); | |
950 host_->SetDuration(duration_); | 929 host_->SetDuration(duration_); |
951 | 930 |
952 ChangeState_Locked(INITIALIZED); | 931 ChangeState_Locked(INITIALIZED); |
953 PipelineStatusCB cb; | 932 PipelineStatusCB cb; |
954 std::swap(cb, init_cb_); | 933 std::swap(cb, init_cb_); |
955 cb.Run(PIPELINE_OK); | 934 cb.Run(PIPELINE_OK); |
956 } | 935 } |
957 | 936 |
958 bool ChunkDemuxer::OnNewConfigs(bool has_audio, bool has_video, | 937 bool ChunkDemuxer::OnNewConfigs(bool has_audio, bool has_video, |
959 const AudioDecoderConfig& audio_config, | 938 const AudioDecoderConfig& audio_config, |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1028 // TODO(vrk): There should be a special case for the first appends where all | 1007 // TODO(vrk): There should be a special case for the first appends where all |
1029 // streams (for both demuxed and muxed case) begin at the earliest stream | 1008 // streams (for both demuxed and muxed case) begin at the earliest stream |
1030 // timestamp. (crbug.com/132815) | 1009 // timestamp. (crbug.com/132815) |
1031 if (audio_ && source_id == source_id_audio_) | 1010 if (audio_ && source_id == source_id_audio_) |
1032 audio_->OnNewMediaSegment(start_timestamp); | 1011 audio_->OnNewMediaSegment(start_timestamp); |
1033 if (video_ && source_id == source_id_video_) | 1012 if (video_ && source_id == source_id_video_) |
1034 video_->OnNewMediaSegment(start_timestamp); | 1013 video_->OnNewMediaSegment(start_timestamp); |
1035 } | 1014 } |
1036 | 1015 |
1037 } // namespace media | 1016 } // namespace media |
OLD | NEW |