| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/filters/ffmpeg_demuxer.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/sparse_histogram.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/sys_byteorder.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "media/audio/sample_rates.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/limits.h" |
| #include "media/base/media_log.h" |
| #include "media/base/media_tracks.h" |
| #include "media/base/timestamp_constants.h" |
| #include "media/base/video_codecs.h" |
| #include "media/ffmpeg/ffmpeg_common.h" |
| #include "media/filters/ffmpeg_aac_bitstream_converter.h" |
| #include "media/filters/ffmpeg_bitstream_converter.h" |
| #include "media/filters/ffmpeg_glue.h" |
| #include "media/filters/ffmpeg_h264_to_annex_b_bitstream_converter.h" |
| #include "media/filters/webvtt_util.h" |
| #include "media/formats/webm/webm_crypto_helpers.h" |
| #include "media/media_features.h" |
| |
| #if BUILDFLAG(ENABLE_HEVC_DEMUXING) |
| #include "media/filters/ffmpeg_h265_to_annex_b_bitstream_converter.h" |
| #endif |
| |
| namespace media { |
| |
| static base::Time ExtractTimelineOffset(AVFormatContext* format_context) { |
| if (strstr(format_context->iformat->name, "webm") || |
| strstr(format_context->iformat->name, "matroska")) { |
| const AVDictionaryEntry* entry = |
| av_dict_get(format_context->metadata, "creation_time", NULL, 0); |
| |
| base::Time timeline_offset; |
| |
| // FFmpegDemuxerTests assume base::Time::FromUTCString() is used here. |
| if (entry != NULL && entry->value != NULL && |
| base::Time::FromUTCString(entry->value, &timeline_offset)) { |
| return timeline_offset; |
| } |
| } |
| |
| return base::Time(); |
| } |
| |
| static base::TimeDelta FramesToTimeDelta(int frames, double sample_rate) { |
| return base::TimeDelta::FromMicroseconds( |
| frames * base::Time::kMicrosecondsPerSecond / sample_rate); |
| } |
| |
| static base::TimeDelta ExtractStartTime(AVStream* stream, |
| base::TimeDelta start_time_estimate) { |
| DCHECK(start_time_estimate != kNoTimestamp); |
| if (stream->start_time == static_cast<int64_t>(AV_NOPTS_VALUE)) { |
| return start_time_estimate == kInfiniteDuration ? kNoTimestamp |
| : start_time_estimate; |
| } |
| |
| // First try the lower of the estimate and the |start_time| value. |
| base::TimeDelta start_time = |
| std::min(ConvertFromTimeBase(stream->time_base, stream->start_time), |
| start_time_estimate); |
| |
| // Next see if the first buffered pts value is usable. |
| if (stream->pts_buffer[0] != static_cast<int64_t>(AV_NOPTS_VALUE)) { |
| const base::TimeDelta buffered_pts = |
| ConvertFromTimeBase(stream->time_base, stream->pts_buffer[0]); |
| if (buffered_pts < start_time) |
| start_time = buffered_pts; |
| } |
| |
| // NOTE: Do not use AVStream->first_dts since |start_time| should be a |
| // presentation timestamp. |
| return start_time; |
| } |
| |
| // Some videos just want to watch the world burn, with a height of 0; cap the |
| // "infinite" aspect ratio resulting. |
| const int kInfiniteRatio = 99999; |
| |
| // Common aspect ratios (multiplied by 100 and truncated) used for histogramming |
| // video sizes. These were taken on 20111103 from |
| // http://wikipedia.org/wiki/Aspect_ratio_(image)#Previous_and_currently_used_aspect_ratios |
| const int kCommonAspectRatios100[] = { |
| 100, 115, 133, 137, 143, 150, 155, 160, 166, |
| 175, 177, 185, 200, 210, 220, 221, 235, 237, |
| 240, 255, 259, 266, 276, 293, 400, 1200, kInfiniteRatio, |
| }; |
| |
| template <class T> // T has int width() & height() methods. |
| static void UmaHistogramAspectRatio(const char* name, const T& size) { |
| UMA_HISTOGRAM_CUSTOM_ENUMERATION( |
| name, |
| // Intentionally use integer division to truncate the result. |
| size.height() ? (size.width() * 100) / size.height() : kInfiniteRatio, |
| base::CustomHistogram::ArrayToCustomRanges( |
| kCommonAspectRatios100, arraysize(kCommonAspectRatios100))); |
| } |
| |
| // Record detected track counts by type corresponding to a src= playback. |
| // Counts are split into 50 buckets, capped into [0,100] range. |
| static void RecordDetectedTrackTypeStats(int audio_count, |
| int video_count, |
| int text_count) { |
| UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Audio", audio_count); |
| UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Video", video_count); |
| UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Text", text_count); |
| } |
| |
| // Record audio decoder config UMA stats corresponding to a src= playback. |
| static void RecordAudioCodecStats(const AudioDecoderConfig& audio_config) { |
| UMA_HISTOGRAM_ENUMERATION("Media.AudioCodec", audio_config.codec(), |
| kAudioCodecMax + 1); |
| UMA_HISTOGRAM_ENUMERATION("Media.AudioSampleFormat", |
| audio_config.sample_format(), kSampleFormatMax + 1); |
| UMA_HISTOGRAM_ENUMERATION("Media.AudioChannelLayout", |
| audio_config.channel_layout(), |
| CHANNEL_LAYOUT_MAX + 1); |
| AudioSampleRate asr; |
| if (ToAudioSampleRate(audio_config.samples_per_second(), &asr)) { |
| UMA_HISTOGRAM_ENUMERATION("Media.AudioSamplesPerSecond", asr, |
| kAudioSampleRateMax + 1); |
| } else { |
| UMA_HISTOGRAM_COUNTS("Media.AudioSamplesPerSecondUnexpected", |
| audio_config.samples_per_second()); |
| } |
| } |
| |
| // Record video decoder config UMA stats corresponding to a src= playback. |
| static void RecordVideoCodecStats(const VideoDecoderConfig& video_config, |
| AVColorRange color_range, |
| MediaLog* media_log) { |
| media_log->RecordRapporWithSecurityOrigin("Media.OriginUrl.SRC.VideoCodec." + |
| GetCodecName(video_config.codec())); |
| |
| UMA_HISTOGRAM_ENUMERATION("Media.VideoCodec", video_config.codec(), |
| kVideoCodecMax + 1); |
| |
| // Drop UNKNOWN because U_H_E() uses one bucket for all values less than 1. |
| if (video_config.profile() >= 0) { |
| UMA_HISTOGRAM_ENUMERATION("Media.VideoCodecProfile", video_config.profile(), |
| VIDEO_CODEC_PROFILE_MAX + 1); |
| } |
| UMA_HISTOGRAM_COUNTS_10000("Media.VideoVisibleWidth", |
| video_config.visible_rect().width()); |
| UmaHistogramAspectRatio("Media.VideoVisibleAspectRatio", |
| video_config.visible_rect()); |
| UMA_HISTOGRAM_ENUMERATION("Media.VideoPixelFormatUnion", |
| video_config.format(), PIXEL_FORMAT_MAX + 1); |
| UMA_HISTOGRAM_ENUMERATION("Media.VideoFrameColorSpace", |
| video_config.color_space(), COLOR_SPACE_MAX + 1); |
| |
| // Note the PRESUBMIT_IGNORE_UMA_MAX below, this silences the PRESUBMIT.py |
| // check for uma enum max usage, since we're abusing |
| // UMA_HISTOGRAM_ENUMERATION to report a discrete value. |
| UMA_HISTOGRAM_ENUMERATION("Media.VideoColorRange", color_range, |
| AVCOL_RANGE_NB); // PRESUBMIT_IGNORE_UMA_MAX |
| } |
| |
| static const char kCodecNone[] = "none"; |
| |
| static const char* GetCodecName(enum AVCodecID id) { |
| const AVCodecDescriptor* codec_descriptor = avcodec_descriptor_get(id); |
| // If the codec name can't be determined, return none for tracking. |
| return codec_descriptor ? codec_descriptor->name : kCodecNone; |
| } |
| |
| static void SetTimeProperty(MediaLogEvent* event, |
| const std::string& key, |
| base::TimeDelta value) { |
| if (value == kInfiniteDuration) |
| event->params.SetString(key, "kInfiniteDuration"); |
| else if (value == kNoTimestamp) |
| event->params.SetString(key, "kNoTimestamp"); |
| else |
| event->params.SetDouble(key, value.InSecondsF()); |
| } |
| |
| std::unique_ptr<FFmpegDemuxerStream> FFmpegDemuxerStream::Create( |
| FFmpegDemuxer* demuxer, |
| AVStream* stream, |
| const scoped_refptr<MediaLog>& media_log) { |
| if (!demuxer || !stream) |
| return nullptr; |
| |
| std::unique_ptr<FFmpegDemuxerStream> demuxer_stream; |
| std::unique_ptr<AudioDecoderConfig> audio_config; |
| std::unique_ptr<VideoDecoderConfig> video_config; |
| |
| if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { |
| audio_config.reset(new AudioDecoderConfig()); |
| |
| // IsValidConfig() checks that the codec is supported and that the channel |
| // layout and sample format are valid. |
| // |
| // TODO(chcunningham): Change AVStreamToAudioDecoderConfig to check |
| // IsValidConfig internally and return a null scoped_ptr if not valid. |
| if (!AVStreamToAudioDecoderConfig(stream, audio_config.get()) || |
| !audio_config->IsValidConfig()) { |
| MEDIA_LOG(ERROR, media_log) |
| << "FFmpegDemuxer: failed creating audio stream"; |
| return nullptr; |
| } |
| |
| MEDIA_LOG(INFO, media_log) << "FFmpegDemuxer: created audio stream, config " |
| << audio_config->AsHumanReadableString(); |
| } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { |
| video_config.reset(new VideoDecoderConfig()); |
| |
| // IsValidConfig() checks that the codec is supported and that the channel |
| // layout and sample format are valid. |
| // |
| // TODO(chcunningham): Change AVStreamToVideoDecoderConfig to check |
| // IsValidConfig internally and return a null scoped_ptr if not valid. |
| if (!AVStreamToVideoDecoderConfig(stream, video_config.get()) || |
| !video_config->IsValidConfig()) { |
| MEDIA_LOG(ERROR, media_log) |
| << "FFmpegDemuxer: failed creating video stream"; |
| return nullptr; |
| } |
| |
| MEDIA_LOG(INFO, media_log) << "FFmpegDemuxer: created video stream, config " |
| << video_config->AsHumanReadableString(); |
| } |
| |
| return base::WrapUnique(new FFmpegDemuxerStream( |
| demuxer, stream, std::move(audio_config), std::move(video_config))); |
| } |
| |
| static void UnmarkEndOfStream(AVFormatContext* format_context) { |
| format_context->pb->eof_reached = 0; |
| } |
| |
| // |
| // FFmpegDemuxerStream |
| // |
| FFmpegDemuxerStream::FFmpegDemuxerStream( |
| FFmpegDemuxer* demuxer, |
| AVStream* stream, |
| std::unique_ptr<AudioDecoderConfig> audio_config, |
| std::unique_ptr<VideoDecoderConfig> video_config) |
| : demuxer_(demuxer), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| stream_(stream), |
| start_time_(kNoTimestamp), |
| audio_config_(audio_config.release()), |
| video_config_(video_config.release()), |
| type_(UNKNOWN), |
| liveness_(LIVENESS_UNKNOWN), |
| end_of_stream_(false), |
| last_packet_timestamp_(kNoTimestamp), |
| last_packet_duration_(kNoTimestamp), |
| video_rotation_(VIDEO_ROTATION_0), |
| is_enabled_(true), |
| waiting_for_keyframe_(false), |
| fixup_negative_timestamps_(false) { |
| DCHECK(demuxer_); |
| |
| bool is_encrypted = false; |
| int rotation = 0; |
| AVDictionaryEntry* rotation_entry = NULL; |
| |
| // Determine our media format. |
| switch (stream->codecpar->codec_type) { |
| case AVMEDIA_TYPE_AUDIO: |
| DCHECK(audio_config_.get() && !video_config_.get()); |
| type_ = AUDIO; |
| is_encrypted = audio_config_->is_encrypted(); |
| break; |
| case AVMEDIA_TYPE_VIDEO: |
| DCHECK(video_config_.get() && !audio_config_.get()); |
| type_ = VIDEO; |
| is_encrypted = video_config_->is_encrypted(); |
| |
| rotation_entry = av_dict_get(stream->metadata, "rotate", NULL, 0); |
| if (rotation_entry && rotation_entry->value && rotation_entry->value[0]) |
| base::StringToInt(rotation_entry->value, &rotation); |
| |
| switch (rotation) { |
| case 0: |
| break; |
| case 90: |
| video_rotation_ = VIDEO_ROTATION_90; |
| break; |
| case 180: |
| video_rotation_ = VIDEO_ROTATION_180; |
| break; |
| case 270: |
| video_rotation_ = VIDEO_ROTATION_270; |
| break; |
| default: |
| LOG(ERROR) << "Unsupported video rotation metadata: " << rotation; |
| break; |
| } |
| |
| break; |
| case AVMEDIA_TYPE_SUBTITLE: |
| DCHECK(!video_config_.get() && !audio_config_.get()); |
| type_ = TEXT; |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| |
| // Calculate the duration. |
| duration_ = ConvertStreamTimestamp(stream->time_base, stream->duration); |
| |
| if (is_encrypted) { |
| AVDictionaryEntry* key = av_dict_get(stream->metadata, "enc_key_id", NULL, |
| 0); |
| DCHECK(key); |
| DCHECK(key->value); |
| if (!key || !key->value) |
| return; |
| base::StringPiece base64_key_id(key->value); |
| std::string enc_key_id; |
| base::Base64Decode(base64_key_id, &enc_key_id); |
| DCHECK(!enc_key_id.empty()); |
| if (enc_key_id.empty()) |
| return; |
| |
| encryption_key_id_.assign(enc_key_id); |
| demuxer_->OnEncryptedMediaInitData(EmeInitDataType::WEBM, enc_key_id); |
| } |
| } |
| |
| FFmpegDemuxerStream::~FFmpegDemuxerStream() { |
| DCHECK(!demuxer_); |
| DCHECK(read_cb_.is_null()); |
| DCHECK(buffer_queue_.IsEmpty()); |
| } |
| |
| void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| if (!demuxer_ || end_of_stream_) { |
| NOTREACHED() << "Attempted to enqueue packet on a stopped stream"; |
| return; |
| } |
| |
| if (waiting_for_keyframe_) { |
| if (packet.get()->flags & AV_PKT_FLAG_KEY) |
| waiting_for_keyframe_ = false; |
| else { |
| DVLOG(3) << "Dropped non-keyframe pts=" << packet->pts; |
| return; |
| } |
| } |
| |
| #if defined(USE_PROPRIETARY_CODECS) |
| // Convert the packet if there is a bitstream filter. |
| if (packet->data && bitstream_converter_ && |
| !bitstream_converter_->ConvertPacket(packet.get())) { |
| LOG(ERROR) << "Format conversion failed."; |
| } |
| #endif |
| |
| // Get side data if any. For now, the only type of side_data is VP8 Alpha. We |
| // keep this generic so that other side_data types in the future can be |
| // handled the same way as well. |
| av_packet_split_side_data(packet.get()); |
| |
| scoped_refptr<DecoderBuffer> buffer; |
| |
| if (type() == DemuxerStream::TEXT) { |
| int id_size = 0; |
| uint8_t* id_data = av_packet_get_side_data( |
| packet.get(), AV_PKT_DATA_WEBVTT_IDENTIFIER, &id_size); |
| |
| int settings_size = 0; |
| uint8_t* settings_data = av_packet_get_side_data( |
| packet.get(), AV_PKT_DATA_WEBVTT_SETTINGS, &settings_size); |
| |
| std::vector<uint8_t> side_data; |
| MakeSideData(id_data, id_data + id_size, |
| settings_data, settings_data + settings_size, |
| &side_data); |
| |
| buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size, |
| side_data.data(), side_data.size()); |
| } else { |
| int side_data_size = 0; |
| uint8_t* side_data = av_packet_get_side_data( |
| packet.get(), AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, &side_data_size); |
| |
| std::unique_ptr<DecryptConfig> decrypt_config; |
| int data_offset = 0; |
| if ((type() == DemuxerStream::AUDIO && audio_config_->is_encrypted()) || |
| (type() == DemuxerStream::VIDEO && video_config_->is_encrypted())) { |
| if (!WebMCreateDecryptConfig( |
| packet->data, packet->size, |
| reinterpret_cast<const uint8_t*>(encryption_key_id_.data()), |
| encryption_key_id_.size(), &decrypt_config, &data_offset)) { |
| LOG(ERROR) << "Creation of DecryptConfig failed."; |
| } |
| } |
| |
| // If a packet is returned by FFmpeg's av_parser_parse2() the packet will |
| // reference inner memory of FFmpeg. As such we should transfer the packet |
| // into memory we control. |
| if (side_data_size > 0) { |
| buffer = DecoderBuffer::CopyFrom(packet.get()->data + data_offset, |
| packet.get()->size - data_offset, |
| side_data, side_data_size); |
| } else { |
| buffer = DecoderBuffer::CopyFrom(packet.get()->data + data_offset, |
| packet.get()->size - data_offset); |
| } |
| |
| int skip_samples_size = 0; |
| const uint32_t* skip_samples_ptr = |
| reinterpret_cast<const uint32_t*>(av_packet_get_side_data( |
| packet.get(), AV_PKT_DATA_SKIP_SAMPLES, &skip_samples_size)); |
| const int kSkipSamplesValidSize = 10; |
| const int kSkipEndSamplesOffset = 1; |
| if (skip_samples_size >= kSkipSamplesValidSize) { |
| // Because FFmpeg rolls codec delay and skip samples into one we can only |
| // allow front discard padding on the first buffer. Otherwise the discard |
| // helper can't figure out which data to discard. See AudioDiscardHelper. |
| int discard_front_samples = base::ByteSwapToLE32(*skip_samples_ptr); |
| if (last_packet_timestamp_ != kNoTimestamp && discard_front_samples) { |
| DLOG(ERROR) << "Skip samples are only allowed for the first packet."; |
| discard_front_samples = 0; |
| } |
| |
| const int discard_end_samples = |
| base::ByteSwapToLE32(*(skip_samples_ptr + kSkipEndSamplesOffset)); |
| const int samples_per_second = |
| audio_decoder_config().samples_per_second(); |
| buffer->set_discard_padding(std::make_pair( |
| FramesToTimeDelta(discard_front_samples, samples_per_second), |
| FramesToTimeDelta(discard_end_samples, samples_per_second))); |
| } |
| |
| if (decrypt_config) |
| buffer->set_decrypt_config(std::move(decrypt_config)); |
| } |
| |
| if (packet->duration >= 0) { |
| buffer->set_duration( |
| ConvertStreamTimestamp(stream_->time_base, packet->duration)); |
| } else { |
| // TODO(wolenetz): Remove when FFmpeg stops returning negative durations. |
| // https://crbug.com/394418 |
| DVLOG(1) << "FFmpeg returned a buffer with a negative duration! " |
| << packet->duration; |
| buffer->set_duration(kNoTimestamp); |
| } |
| |
| // Note: If pts is AV_NOPTS_VALUE, stream_timestamp will be kNoTimestamp. |
| const base::TimeDelta stream_timestamp = |
| ConvertStreamTimestamp(stream_->time_base, packet->pts); |
| |
| if (stream_timestamp != kNoTimestamp) { |
| const bool is_audio = type() == AUDIO; |
| |
| // If this file has negative timestamps don't rebase any other stream types |
| // against the negative starting time. |
| base::TimeDelta start_time = demuxer_->start_time(); |
| if (fixup_negative_timestamps_ && !is_audio && |
| start_time < base::TimeDelta()) { |
| start_time = base::TimeDelta(); |
| } |
| |
| // Don't rebase timestamps for positive start times, the HTML Media Spec |
| // details this in section "4.8.10.6 Offsets into the media resource." We |
| // will still need to rebase timestamps before seeking with FFmpeg though. |
| if (start_time > base::TimeDelta()) |
| start_time = base::TimeDelta(); |
| |
| buffer->set_timestamp(stream_timestamp - start_time); |
| |
| // If enabled, and no codec delay is present, mark audio packets with |
| // negative timestamps for post-decode discard. |
| if (fixup_negative_timestamps_ && is_audio && |
| stream_timestamp < base::TimeDelta() && |
| buffer->duration() != kNoTimestamp) { |
| if (!audio_decoder_config().codec_delay()) { |
| DCHECK_EQ(buffer->discard_padding().first, base::TimeDelta()); |
| |
| if (stream_timestamp + buffer->duration() < base::TimeDelta()) { |
| DCHECK_EQ(buffer->discard_padding().second, base::TimeDelta()); |
| |
| // Discard the entire packet if it's entirely before zero. |
| buffer->set_discard_padding( |
| std::make_pair(kInfiniteDuration, base::TimeDelta())); |
| } else { |
| // Only discard part of the frame if it overlaps zero. |
| buffer->set_discard_padding(std::make_pair( |
| -stream_timestamp, buffer->discard_padding().second)); |
| } |
| } else { |
| // Verify that codec delay would cover discard and that we don't need to |
| // mark the packet for post decode discard. Since timestamps may be in |
| // milliseconds and codec delay in nanosecond precision, round up to the |
| // nearest millisecond. See enable_negative_timestamp_fixups(). |
| DCHECK_LE(-std::ceil(FramesToTimeDelta( |
| audio_decoder_config().codec_delay(), |
| audio_decoder_config().samples_per_second()) |
| .InMillisecondsF()), |
| stream_timestamp.InMillisecondsF()); |
| } |
| } |
| } else { |
| // If this happens on the first packet, decoders will throw an error. |
| buffer->set_timestamp(kNoTimestamp); |
| } |
| |
| if (last_packet_timestamp_ != kNoTimestamp) { |
| // FFmpeg doesn't support chained ogg correctly. Instead of guaranteeing |
| // continuity across links in the chain it uses the timestamp information |
| // from each link directly. Doing so can lead to timestamps which appear to |
| // go backwards in time. |
| // |
| // If the new link starts with a negative timestamp or a timestamp less than |
| // the original (positive) |start_time|, we will get a negative timestamp |
| // here. It's also possible FFmpeg returns kNoTimestamp here if it's not |
| // able to work out a timestamp using the previous link and the next. |
| // |
| // Fixing chained ogg is non-trivial, so for now just reuse the last good |
| // timestamp. The decoder will rewrite the timestamps to be sample accurate |
| // later. See http://crbug.com/396864. |
| if (fixup_negative_timestamps_ && |
| (buffer->timestamp() == kNoTimestamp || |
| buffer->timestamp() < last_packet_timestamp_)) { |
| buffer->set_timestamp(last_packet_timestamp_ + |
| (last_packet_duration_ != kNoTimestamp |
| ? last_packet_duration_ |
| : base::TimeDelta::FromMicroseconds(1))); |
| } |
| |
| // The demuxer should always output positive timestamps. |
| DCHECK(buffer->timestamp() >= base::TimeDelta()); |
| DCHECK(buffer->timestamp() != kNoTimestamp); |
| |
| if (last_packet_timestamp_ < buffer->timestamp()) { |
| buffered_ranges_.Add(last_packet_timestamp_, buffer->timestamp()); |
| demuxer_->NotifyBufferingChanged(); |
| } |
| } |
| |
| if (packet.get()->flags & AV_PKT_FLAG_KEY) |
| buffer->set_is_key_frame(true); |
| |
| last_packet_timestamp_ = buffer->timestamp(); |
| last_packet_duration_ = buffer->duration(); |
| |
| buffer_queue_.Push(buffer); |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::SetEndOfStream() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| end_of_stream_ = true; |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::FlushBuffers() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(read_cb_.is_null()) << "There should be no pending read"; |
| |
| // H264 and AAC require that we resend the header after flush. |
| // Reset bitstream for converter to do so. |
| // This is related to chromium issue 140371 (http://crbug.com/140371). |
| ResetBitstreamConverter(); |
| |
| buffer_queue_.Clear(); |
| end_of_stream_ = false; |
| last_packet_timestamp_ = kNoTimestamp; |
| last_packet_duration_ = kNoTimestamp; |
| } |
| |
| void FFmpegDemuxerStream::Abort() { |
| if (!read_cb_.is_null()) |
| base::ResetAndReturn(&read_cb_).Run(DemuxerStream::kAborted, nullptr); |
| } |
| |
| void FFmpegDemuxerStream::Stop() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| buffer_queue_.Clear(); |
| if (!read_cb_.is_null()) { |
| base::ResetAndReturn(&read_cb_).Run( |
| DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer()); |
| } |
| demuxer_ = NULL; |
| stream_ = NULL; |
| end_of_stream_ = true; |
| } |
| |
| DemuxerStream::Type FFmpegDemuxerStream::type() const { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return type_; |
| } |
| |
| DemuxerStream::Liveness FFmpegDemuxerStream::liveness() const { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return liveness_; |
| } |
| |
| void FFmpegDemuxerStream::Read(const ReadCB& read_cb) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| CHECK(read_cb_.is_null()) << "Overlapping reads are not supported"; |
| read_cb_ = BindToCurrentLoop(read_cb); |
| |
| // Don't accept any additional reads if we've been told to stop. |
| // The |demuxer_| may have been destroyed in the pipeline thread. |
| // |
| // TODO(scherkus): it would be cleaner to reply with an error message. |
| if (!demuxer_) { |
| base::ResetAndReturn(&read_cb_).Run( |
| DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer()); |
| return; |
| } |
| |
| if (!is_enabled_) { |
| DVLOG(1) << "Read from disabled stream, returning EOS"; |
| base::ResetAndReturn(&read_cb_).Run(kOk, DecoderBuffer::CreateEOSBuffer()); |
| return; |
| } |
| |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::EnableBitstreamConverter() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| #if defined(USE_PROPRIETARY_CODECS) |
| InitBitstreamConverter(); |
| #else |
| NOTREACHED() << "Proprietary codecs not enabled."; |
| #endif |
| } |
| |
| void FFmpegDemuxerStream::ResetBitstreamConverter() { |
| #if defined(USE_PROPRIETARY_CODECS) |
| if (bitstream_converter_) |
| InitBitstreamConverter(); |
| #endif // defined(USE_PROPRIETARY_CODECS) |
| } |
| |
| void FFmpegDemuxerStream::InitBitstreamConverter() { |
| #if defined(USE_PROPRIETARY_CODECS) |
| switch (stream_->codecpar->codec_id) { |
| case AV_CODEC_ID_H264: |
| // Clear |extra_data| so that future (fallback) decoders will know that |
| // conversion is forcibly enabled on this stream. |
| // |
| // TODO(sandersd): Ideally we would convert |extra_data| to concatenated |
| // SPS/PPS data, but it's too late to be useful because Initialize() was |
| // already called on GpuVideoDecoder, which is the only path that would |
| // consume that data. |
| if (video_config_) |
| video_config_->SetExtraData(std::vector<uint8_t>()); |
| bitstream_converter_.reset( |
| new FFmpegH264ToAnnexBBitstreamConverter(stream_->codecpar)); |
| break; |
| #if BUILDFLAG(ENABLE_HEVC_DEMUXING) |
| case AV_CODEC_ID_HEVC: |
| bitstream_converter_.reset( |
| new FFmpegH265ToAnnexBBitstreamConverter(stream_->codecpar)); |
| break; |
| #endif |
| case AV_CODEC_ID_AAC: |
| bitstream_converter_.reset( |
| new FFmpegAACBitstreamConverter(stream_->codecpar)); |
| break; |
| default: |
| break; |
| } |
| #endif // defined(USE_PROPRIETARY_CODECS) |
| } |
| |
| bool FFmpegDemuxerStream::SupportsConfigChanges() { return false; } |
| |
| AudioDecoderConfig FFmpegDemuxerStream::audio_decoder_config() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_EQ(type_, AUDIO); |
| DCHECK(audio_config_.get()); |
| return *audio_config_; |
| } |
| |
| VideoDecoderConfig FFmpegDemuxerStream::video_decoder_config() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_EQ(type_, VIDEO); |
| DCHECK(video_config_.get()); |
| return *video_config_; |
| } |
| |
| VideoRotation FFmpegDemuxerStream::video_rotation() { |
| return video_rotation_; |
| } |
| |
| bool FFmpegDemuxerStream::enabled() const { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return is_enabled_; |
| } |
| |
| void FFmpegDemuxerStream::set_enabled(bool enabled, base::TimeDelta timestamp) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (enabled == is_enabled_) |
| return; |
| |
| is_enabled_ = enabled; |
| if (is_enabled_) { |
| waiting_for_keyframe_ = true; |
| } |
| if (!is_enabled_ && !read_cb_.is_null()) { |
| DVLOG(1) << "Read from disabled stream, returning EOS"; |
| base::ResetAndReturn(&read_cb_).Run(kOk, DecoderBuffer::CreateEOSBuffer()); |
| return; |
| } |
| if (!stream_status_change_cb_.is_null()) |
| stream_status_change_cb_.Run(is_enabled_, timestamp); |
| } |
| |
| void FFmpegDemuxerStream::SetStreamStatusChangeCB( |
| const StreamStatusChangeCB& cb) { |
| DCHECK(!cb.is_null()); |
| stream_status_change_cb_ = cb; |
| } |
| |
| void FFmpegDemuxerStream::SetLiveness(Liveness liveness) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_EQ(liveness_, LIVENESS_UNKNOWN); |
| liveness_ = liveness; |
| } |
| |
| base::TimeDelta FFmpegDemuxerStream::GetElapsedTime() const { |
| return ConvertStreamTimestamp(stream_->time_base, stream_->cur_dts); |
| } |
| |
| Ranges<base::TimeDelta> FFmpegDemuxerStream::GetBufferedRanges() const { |
| return buffered_ranges_; |
| } |
| |
| void FFmpegDemuxerStream::SatisfyPendingRead() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (!read_cb_.is_null()) { |
| if (!buffer_queue_.IsEmpty()) { |
| base::ResetAndReturn(&read_cb_).Run( |
| DemuxerStream::kOk, buffer_queue_.Pop()); |
| } else if (end_of_stream_) { |
| base::ResetAndReturn(&read_cb_).Run( |
| DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer()); |
| } |
| } |
| |
| // Have capacity? Ask for more! |
| if (HasAvailableCapacity() && !end_of_stream_) { |
| demuxer_->NotifyCapacityAvailable(); |
| } |
| } |
| |
| bool FFmpegDemuxerStream::HasAvailableCapacity() { |
| // TODO(scherkus): Remove this return and reenable time-based capacity |
| // after our data sources support canceling/concurrent reads, see |
| // http://crbug.com/165762 for details. |
| #if 1 |
| return !read_cb_.is_null(); |
| #else |
| // Try to have one second's worth of encoded data per stream. |
| const base::TimeDelta kCapacity = base::TimeDelta::FromSeconds(1); |
| return buffer_queue_.IsEmpty() || buffer_queue_.Duration() < kCapacity; |
| #endif |
| } |
| |
| size_t FFmpegDemuxerStream::MemoryUsage() const { |
| return buffer_queue_.data_size(); |
| } |
| |
| TextKind FFmpegDemuxerStream::GetTextKind() const { |
| DCHECK_EQ(type_, DemuxerStream::TEXT); |
| |
| if (stream_->disposition & AV_DISPOSITION_CAPTIONS) |
| return kTextCaptions; |
| |
| if (stream_->disposition & AV_DISPOSITION_DESCRIPTIONS) |
| return kTextDescriptions; |
| |
| if (stream_->disposition & AV_DISPOSITION_METADATA) |
| return kTextMetadata; |
| |
| return kTextSubtitles; |
| } |
| |
| std::string FFmpegDemuxerStream::GetMetadata(const char* key) const { |
| const AVDictionaryEntry* entry = |
| av_dict_get(stream_->metadata, key, NULL, 0); |
| return (entry == NULL || entry->value == NULL) ? "" : entry->value; |
| } |
| |
| // static |
| base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp( |
| const AVRational& time_base, |
| int64_t timestamp) { |
| if (timestamp == static_cast<int64_t>(AV_NOPTS_VALUE)) |
| return kNoTimestamp; |
| |
| return ConvertFromTimeBase(time_base, timestamp); |
| } |
| |
| // |
| // FFmpegDemuxer |
| // |
| FFmpegDemuxer::FFmpegDemuxer( |
| const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, |
| DataSource* data_source, |
| const EncryptedMediaInitDataCB& encrypted_media_init_data_cb, |
| const MediaTracksUpdatedCB& media_tracks_updated_cb, |
| const scoped_refptr<MediaLog>& media_log) |
| : host_(NULL), |
| task_runner_(task_runner), |
| blocking_thread_("FFmpegDemuxer"), |
| pending_read_(false), |
| data_source_(data_source), |
| media_log_(media_log), |
| bitrate_(0), |
| start_time_(kNoTimestamp), |
| text_enabled_(false), |
| duration_known_(false), |
| encrypted_media_init_data_cb_(encrypted_media_init_data_cb), |
| media_tracks_updated_cb_(media_tracks_updated_cb), |
| cancel_pending_seek_factory_(this), |
| weak_factory_(this) { |
| DCHECK(task_runner_.get()); |
| DCHECK(data_source_); |
| DCHECK(!media_tracks_updated_cb_.is_null()); |
| } |
| |
| FFmpegDemuxer::~FFmpegDemuxer() { |
| // NOTE: This class is not destroyed on |task_runner|, so we must ensure that |
| // there are no outstanding WeakPtrs by the time we reach here. |
| DCHECK(!weak_factory_.HasWeakPtrs()); |
| } |
| |
| std::string FFmpegDemuxer::GetDisplayName() const { |
| return "FFmpegDemuxer"; |
| } |
| |
| void FFmpegDemuxer::Initialize(DemuxerHost* host, |
| const PipelineStatusCB& status_cb, |
| bool enable_text_tracks) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| host_ = host; |
| text_enabled_ = enable_text_tracks; |
| weak_this_ = cancel_pending_seek_factory_.GetWeakPtr(); |
| |
| url_protocol_.reset(new BlockingUrlProtocol( |
| data_source_, |
| BindToCurrentLoop(base::Bind(&FFmpegDemuxer::OnDataSourceError, |
| base::Unretained(this))))); |
| glue_.reset(new FFmpegGlue(url_protocol_.get())); |
| AVFormatContext* format_context = glue_->format_context(); |
| |
| // Disable ID3v1 tag reading to avoid costly seeks to end of file for data we |
| // don't use. FFmpeg will only read ID3v1 tags if no other metadata is |
| // available, so add a metadata entry to ensure some is always present. |
| av_dict_set(&format_context->metadata, "skip_id3v1_tags", "", 0); |
| |
| // Ensure ffmpeg doesn't give up too early while looking for stream params; |
| // this does not increase the amount of data downloaded. The default value |
| // is 5 AV_TIME_BASE units (1 second each), which prevents some oddly muxed |
| // streams from being detected properly; this value was chosen arbitrarily. |
| format_context->max_analyze_duration = 60 * AV_TIME_BASE; |
| |
| // Open the AVFormatContext using our glue layer. |
| CHECK(blocking_thread_.Start()); |
| base::PostTaskAndReplyWithResult( |
| blocking_thread_.task_runner().get(), FROM_HERE, |
| base::Bind(&FFmpegGlue::OpenContext, base::Unretained(glue_.get())), |
| base::Bind(&FFmpegDemuxer::OnOpenContextDone, weak_factory_.GetWeakPtr(), |
| status_cb)); |
| } |
| |
| void FFmpegDemuxer::AbortPendingReads() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| // If Stop() has been called, then drop this call. |
| if (!blocking_thread_.IsRunning()) |
| return; |
| |
| // This should only be called after the demuxer has been initialized. |
| DCHECK_GT(streams_.size(), 0u); |
| |
| // Abort all outstanding reads. |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->Abort(); |
| } |
| |
| // It's important to invalidate read/seek completion callbacks to avoid any |
| // errors that occur because of the data source abort. |
| weak_factory_.InvalidateWeakPtrs(); |
| data_source_->Abort(); |
| |
| // Aborting the read may cause EOF to be marked, undo this. |
| blocking_thread_.task_runner()->PostTask( |
| FROM_HERE, base::Bind(&UnmarkEndOfStream, glue_->format_context())); |
| pending_read_ = false; |
| |
| // TODO(dalecurtis): We probably should report PIPELINE_ERROR_ABORT here |
| // instead to avoid any preroll work that may be started upon return, but |
| // currently the PipelineImpl does not know how to handle this. |
| if (!pending_seek_cb_.is_null()) |
| base::ResetAndReturn(&pending_seek_cb_).Run(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::Stop() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| // The order of Stop() and Abort() is important here. If Abort() is called |
| // first, control may pass into FFmpeg where it can destruct buffers that are |
| // in the process of being fulfilled by the DataSource. |
| data_source_->Stop(); |
| url_protocol_->Abort(); |
| |
| // This will block until all tasks complete. Note that after this returns it's |
| // possible for reply tasks (e.g., OnReadFrameDone()) to be queued on this |
| // thread. Each of the reply task methods must check whether we've stopped the |
| // thread and drop their results on the floor. |
| blocking_thread_.Stop(); |
| |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->Stop(); |
| } |
| |
| data_source_ = NULL; |
| |
| // Invalidate WeakPtrs on |task_runner_|, destruction may happen on another |
| // thread. |
| weak_factory_.InvalidateWeakPtrs(); |
| cancel_pending_seek_factory_.InvalidateWeakPtrs(); |
| } |
| |
| void FFmpegDemuxer::StartWaitingForSeek(base::TimeDelta seek_time) {} |
| |
| void FFmpegDemuxer::CancelPendingSeek(base::TimeDelta seek_time) { |
| if (task_runner_->BelongsToCurrentThread()) { |
| AbortPendingReads(); |
| } else { |
| // Don't use GetWeakPtr() here since we are on the wrong thread. |
| task_runner_->PostTask( |
| FROM_HERE, base::Bind(&FFmpegDemuxer::AbortPendingReads, weak_this_)); |
| } |
| } |
| |
| void FFmpegDemuxer::Seek(base::TimeDelta time, const PipelineStatusCB& cb) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| CHECK(pending_seek_cb_.is_null()); |
| |
| // FFmpeg requires seeks to be adjusted according to the lowest starting time. |
| // Since EnqueuePacket() rebased negative timestamps by the start time, we |
| // must correct the shift here. |
| // |
| // Additionally, to workaround limitations in how we expose seekable ranges to |
| // Blink (http://crbug.com/137275), we also want to clamp seeks before the |
| // start time to the start time. |
| base::TimeDelta seek_time = start_time_ < base::TimeDelta() |
| ? time + start_time_ |
| : time < start_time_ ? start_time_ : time; |
| |
| // When seeking in an opus stream we need to ensure we deliver enough data to |
| // satisfy the seek preroll; otherwise the audio at the actual seek time will |
| // not be entirely accurate. |
| FFmpegDemuxerStream* audio_stream = GetFFmpegStream(DemuxerStream::AUDIO); |
| if (audio_stream) { |
| const AudioDecoderConfig& config = audio_stream->audio_decoder_config(); |
| if (config.codec() == kCodecOpus) |
| seek_time = std::max(start_time_, seek_time - config.seek_preroll()); |
| } |
| |
| // Choose the seeking stream based on whether it contains the seek time, if no |
| // match can be found prefer the preferred stream. |
| // |
| // TODO(dalecurtis): Currently FFmpeg does not ensure that all streams in a |
| // given container will demux all packets after the seek point. Instead it |
| // only guarantees that all packets after the file position of the seek will |
| // be demuxed. It's an open question whether FFmpeg should fix this: |
| // http://lists.ffmpeg.org/pipermail/ffmpeg-devel/2014-June/159212.html |
| // Tracked by http://crbug.com/387996. |
| FFmpegDemuxerStream* demux_stream = FindPreferredStreamForSeeking(seek_time); |
| DCHECK(demux_stream); |
| const AVStream* seeking_stream = demux_stream->av_stream(); |
| DCHECK(seeking_stream); |
| |
| pending_seek_cb_ = cb; |
| base::PostTaskAndReplyWithResult( |
| blocking_thread_.task_runner().get(), FROM_HERE, |
| base::Bind(&av_seek_frame, glue_->format_context(), seeking_stream->index, |
| ConvertToTimeBase(seeking_stream->time_base, seek_time), |
| // Always seek to a timestamp <= to the desired timestamp. |
| AVSEEK_FLAG_BACKWARD), |
| base::Bind(&FFmpegDemuxer::OnSeekFrameDone, weak_factory_.GetWeakPtr())); |
| } |
| |
| base::Time FFmpegDemuxer::GetTimelineOffset() const { |
| return timeline_offset_; |
| } |
| |
| DemuxerStream* FFmpegDemuxer::GetStream(DemuxerStream::Type type) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return GetFFmpegStream(type); |
| } |
| |
| FFmpegDemuxerStream* FFmpegDemuxer::GetFFmpegStream( |
| DemuxerStream::Type type) const { |
| for (const auto& stream : streams_) { |
| if (stream && stream->type() == type && stream->enabled()) { |
| return stream.get(); |
| } |
| } |
| return NULL; |
| } |
| |
| base::TimeDelta FFmpegDemuxer::GetStartTime() const { |
| return std::max(start_time_, base::TimeDelta()); |
| } |
| |
| void FFmpegDemuxer::AddTextStreams() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| for (const auto& stream : streams_) { |
| if (!stream || stream->type() != DemuxerStream::TEXT) |
| continue; |
| |
| TextKind kind = stream->GetTextKind(); |
| std::string title = stream->GetMetadata("title"); |
| std::string language = stream->GetMetadata("language"); |
| |
| // TODO: Implement "id" metadata in FFMPEG. |
| // See: http://crbug.com/323183 |
| host_->AddTextStream(stream.get(), |
| TextTrackConfig(kind, title, language, std::string())); |
| } |
| } |
| |
| int64_t FFmpegDemuxer::GetMemoryUsage() const { |
| int64_t allocation_size = 0; |
| for (const auto& stream : streams_) { |
| if (stream) |
| allocation_size += stream->MemoryUsage(); |
| } |
| return allocation_size; |
| } |
| |
| void FFmpegDemuxer::OnEncryptedMediaInitData( |
| EmeInitDataType init_data_type, |
| const std::string& encryption_key_id) { |
| std::vector<uint8_t> key_id_local(encryption_key_id.begin(), |
| encryption_key_id.end()); |
| encrypted_media_init_data_cb_.Run(init_data_type, key_id_local); |
| } |
| |
| void FFmpegDemuxer::NotifyCapacityAvailable() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| ReadFrameIfNeeded(); |
| } |
| |
| void FFmpegDemuxer::NotifyBufferingChanged() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| Ranges<base::TimeDelta> buffered; |
| FFmpegDemuxerStream* audio = GetFFmpegStream(DemuxerStream::AUDIO); |
| FFmpegDemuxerStream* video = GetFFmpegStream(DemuxerStream::VIDEO); |
| if (audio && video) { |
| buffered = |
| audio->GetBufferedRanges().IntersectionWith(video->GetBufferedRanges()); |
| } else if (audio) { |
| buffered = audio->GetBufferedRanges(); |
| } else if (video) { |
| buffered = video->GetBufferedRanges(); |
| } |
| host_->OnBufferedTimeRangesChanged(buffered); |
| } |
| |
| // Helper for calculating the bitrate of the media based on information stored |
| // in |format_context| or failing that the size and duration of the media. |
| // |
| // Returns 0 if a bitrate could not be determined. |
| static int CalculateBitrate(AVFormatContext* format_context, |
| const base::TimeDelta& duration, |
| int64_t filesize_in_bytes) { |
| // If there is a bitrate set on the container, use it. |
| if (format_context->bit_rate > 0) |
| return format_context->bit_rate; |
| |
| // Then try to sum the bitrates individually per stream. |
| int bitrate = 0; |
| for (size_t i = 0; i < format_context->nb_streams; ++i) { |
| AVCodecParameters* codec_parameters = format_context->streams[i]->codecpar; |
| bitrate += codec_parameters->bit_rate; |
| } |
| if (bitrate > 0) |
| return bitrate; |
| |
| // See if we can approximate the bitrate as long as we have a filesize and |
| // valid duration. |
| if (duration.InMicroseconds() <= 0 || duration == kInfiniteDuration || |
| filesize_in_bytes == 0) { |
| return 0; |
| } |
| |
| // Do math in floating point as we'd overflow an int64_t if the filesize was |
| // larger than ~1073GB. |
| double bytes = filesize_in_bytes; |
| double duration_us = duration.InMicroseconds(); |
| return bytes * 8000000.0 / duration_us; |
| } |
| |
| void FFmpegDemuxer::OnOpenContextDone(const PipelineStatusCB& status_cb, |
| bool result) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (!blocking_thread_.IsRunning()) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| status_cb.Run(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| if (!result) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": open context failed"; |
| status_cb.Run(DEMUXER_ERROR_COULD_NOT_OPEN); |
| return; |
| } |
| |
| // Fully initialize AVFormatContext by parsing the stream a little. |
| base::PostTaskAndReplyWithResult( |
| blocking_thread_.task_runner().get(), |
| FROM_HERE, |
| base::Bind(&avformat_find_stream_info, |
| glue_->format_context(), |
| static_cast<AVDictionary**>(NULL)), |
| base::Bind(&FFmpegDemuxer::OnFindStreamInfoDone, |
| weak_factory_.GetWeakPtr(), |
| status_cb)); |
| } |
| |
| void FFmpegDemuxer::OnFindStreamInfoDone(const PipelineStatusCB& status_cb, |
| int result) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (!blocking_thread_.IsRunning() || !data_source_) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| status_cb.Run(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| if (result < 0) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() |
| << ": find stream info failed"; |
| status_cb.Run(DEMUXER_ERROR_COULD_NOT_PARSE); |
| return; |
| } |
| |
| // Create demuxer stream entries for each possible AVStream. Each stream |
| // is examined to determine if it is supported or not (is the codec enabled |
| // for it in this release?). Unsupported streams are skipped, allowing for |
| // partial playback. At least one audio or video stream must be playable. |
| AVFormatContext* format_context = glue_->format_context(); |
| streams_.resize(format_context->nb_streams); |
| |
| // Estimate the start time for each stream by looking through the packets |
| // buffered during avformat_find_stream_info(). These values will be |
| // considered later when determining the actual stream start time. |
| // |
| // These packets haven't been completely processed yet, so only look through |
| // these values if the AVFormatContext has a valid start time. |
| // |
| // If no estimate is found, the stream entry will be kInfiniteDuration. |
| std::vector<base::TimeDelta> start_time_estimates(format_context->nb_streams, |
| kInfiniteDuration); |
| const AVFormatInternal* internal = format_context->internal; |
| if (internal && internal->packet_buffer && |
| format_context->start_time != static_cast<int64_t>(AV_NOPTS_VALUE)) { |
| struct AVPacketList* packet_buffer = internal->packet_buffer; |
| while (packet_buffer != internal->packet_buffer_end) { |
| DCHECK_LT(static_cast<size_t>(packet_buffer->pkt.stream_index), |
| start_time_estimates.size()); |
| const AVStream* stream = |
| format_context->streams[packet_buffer->pkt.stream_index]; |
| if (packet_buffer->pkt.pts != static_cast<int64_t>(AV_NOPTS_VALUE)) { |
| const base::TimeDelta packet_pts = |
| ConvertFromTimeBase(stream->time_base, packet_buffer->pkt.pts); |
| if (packet_pts < start_time_estimates[stream->index]) |
| start_time_estimates[stream->index] = packet_pts; |
| } |
| packet_buffer = packet_buffer->next; |
| } |
| } |
| |
| std::unique_ptr<MediaTracks> media_tracks(new MediaTracks()); |
| |
| DCHECK(track_id_to_demux_stream_map_.empty()); |
| |
| // If available, |start_time_| will be set to the lowest stream start time. |
| start_time_ = kInfiniteDuration; |
| |
| base::TimeDelta max_duration; |
| int detected_audio_track_count = 0; |
| int detected_video_track_count = 0; |
| int detected_text_track_count = 0; |
| for (size_t i = 0; i < format_context->nb_streams; ++i) { |
| AVStream* stream = format_context->streams[i]; |
| const AVCodecParameters* codec_parameters = stream->codecpar; |
| const AVMediaType codec_type = codec_parameters->codec_type; |
| const AVCodecID codec_id = codec_parameters->codec_id; |
| |
| if (codec_type == AVMEDIA_TYPE_AUDIO) { |
| // Log the codec detected, whether it is supported or not, and whether or |
| // not we have already detected a supported codec in another stream. |
| UMA_HISTOGRAM_SPARSE_SLOWLY("Media.DetectedAudioCodecHash", |
| HashCodecName(GetCodecName(codec_id))); |
| detected_audio_track_count++; |
| } else if (codec_type == AVMEDIA_TYPE_VIDEO) { |
| // Log the codec detected, whether it is supported or not, and whether or |
| // not we have already detected a supported codec in another stream. |
| UMA_HISTOGRAM_SPARSE_SLOWLY("Media.DetectedVideoCodecHash", |
| HashCodecName(GetCodecName(codec_id))); |
| detected_video_track_count++; |
| |
| #if BUILDFLAG(ENABLE_HEVC_DEMUXING) |
| if (codec_id == AV_CODEC_ID_HEVC) { |
| // If ffmpeg is built without HEVC parser/decoder support, it will be |
| // able to demux HEVC based solely on container-provided information, |
| // but unable to get some of the parameters without parsing the stream |
| // (e.g. coded size needs to be read from SPS, pixel format is typically |
| // deduced from decoder config in hvcC box). These are not really needed |
| // when using external decoder (e.g. hardware decoder), so override them |
| // to make sure this translates into a valid VideoDecoderConfig. Coded |
| // size is overridden in AVStreamToVideoDecoderConfig(). |
| if (stream->codecpar->format == AV_PIX_FMT_NONE) |
| stream->codecpar->format = AV_PIX_FMT_YUV420P; |
| } |
| #endif |
| } else if (codec_type == AVMEDIA_TYPE_SUBTITLE) { |
| detected_text_track_count++; |
| if (codec_id != AV_CODEC_ID_WEBVTT || !text_enabled_) { |
| continue; |
| } |
| } else { |
| continue; |
| } |
| |
| // Attempt to create a FFmpegDemuxerStream from the AVStream. This will |
| // return nullptr if the AVStream is invalid. Validity checks will verify |
| // things like: codec, channel layout, sample/pixel format, etc... |
| std::unique_ptr<FFmpegDemuxerStream> demuxer_stream = |
| FFmpegDemuxerStream::Create(this, stream, media_log_); |
| if (demuxer_stream.get()) { |
| streams_[i] = std::move(demuxer_stream); |
| } else { |
| if (codec_type == AVMEDIA_TYPE_AUDIO) { |
| MEDIA_LOG(INFO, media_log_) |
| << GetDisplayName() |
| << ": skipping invalid or unsupported audio track"; |
| } else if (codec_type == AVMEDIA_TYPE_VIDEO) { |
| MEDIA_LOG(INFO, media_log_) |
| << GetDisplayName() |
| << ": skipping invalid or unsupported video track"; |
| } |
| |
| // This AVStream does not successfully convert. |
| continue; |
| } |
| |
| StreamParser::TrackId track_id = stream->id; |
| std::string track_label = streams_[i]->GetMetadata("handler_name"); |
| std::string track_language = streams_[i]->GetMetadata("language"); |
| |
| // Some metadata is named differently in FFmpeg for webm files. |
| if (strstr(format_context->iformat->name, "webm") || |
| strstr(format_context->iformat->name, "matroska")) { |
| // TODO(servolk): FFmpeg doesn't set stream->id correctly for webm files. |
| // Need to fix that and use it as track id. crbug.com/323183 |
| track_id = |
| static_cast<StreamParser::TrackId>(media_tracks->tracks().size() + 1); |
| track_label = streams_[i]->GetMetadata("title"); |
| } |
| |
| if ((codec_type == AVMEDIA_TYPE_AUDIO && |
| media_tracks->getAudioConfig(track_id).IsValidConfig()) || |
| (codec_type == AVMEDIA_TYPE_VIDEO && |
| media_tracks->getVideoConfig(track_id).IsValidConfig())) { |
| MEDIA_LOG(INFO, media_log_) |
| << GetDisplayName() |
| << ": skipping duplicate media stream id=" << track_id; |
| continue; |
| } |
| |
| // Note when we find our audio/video stream (we only want one of each) and |
| // record src= playback UMA stats for the stream's decoder config. |
| MediaTrack* media_track = nullptr; |
| if (codec_type == AVMEDIA_TYPE_AUDIO) { |
| AudioDecoderConfig audio_config = streams_[i]->audio_decoder_config(); |
| RecordAudioCodecStats(audio_config); |
| |
| media_track = media_tracks->AddAudioTrack(audio_config, track_id, "main", |
| track_label, track_language); |
| media_track->set_id(base::UintToString(track_id)); |
| DCHECK(track_id_to_demux_stream_map_.find(media_track->id()) == |
| track_id_to_demux_stream_map_.end()); |
| track_id_to_demux_stream_map_[media_track->id()] = streams_[i].get(); |
| } else if (codec_type == AVMEDIA_TYPE_VIDEO) { |
| VideoDecoderConfig video_config = streams_[i]->video_decoder_config(); |
| |
| RecordVideoCodecStats(video_config, stream->codecpar->color_range, |
| media_log_.get()); |
| |
| media_track = media_tracks->AddVideoTrack(video_config, track_id, "main", |
| track_label, track_language); |
| media_track->set_id(base::UintToString(track_id)); |
| DCHECK(track_id_to_demux_stream_map_.find(media_track->id()) == |
| track_id_to_demux_stream_map_.end()); |
| track_id_to_demux_stream_map_[media_track->id()] = streams_[i].get(); |
| } |
| |
| max_duration = std::max(max_duration, streams_[i]->duration()); |
| |
| const base::TimeDelta start_time = |
| ExtractStartTime(stream, start_time_estimates[i]); |
| const bool has_start_time = start_time != kNoTimestamp; |
| |
| if (!has_start_time) |
| continue; |
| |
| streams_[i]->set_start_time(start_time); |
| if (start_time < start_time_) { |
| start_time_ = start_time; |
| } |
| } |
| |
| RecordDetectedTrackTypeStats(detected_audio_track_count, |
| detected_video_track_count, |
| detected_text_track_count); |
| |
| if (media_tracks->tracks().empty()) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() |
| << ": no supported streams"; |
| status_cb.Run(DEMUXER_ERROR_NO_SUPPORTED_STREAMS); |
| return; |
| } |
| |
| if (text_enabled_) |
| AddTextStreams(); |
| |
| if (format_context->duration != static_cast<int64_t>(AV_NOPTS_VALUE)) { |
| // If there is a duration value in the container use that to find the |
| // maximum between it and the duration from A/V streams. |
| const AVRational av_time_base = {1, AV_TIME_BASE}; |
| max_duration = |
| std::max(max_duration, |
| ConvertFromTimeBase(av_time_base, format_context->duration)); |
| } else { |
| // The duration is unknown, in which case this is likely a live stream. |
| max_duration = kInfiniteDuration; |
| } |
| |
| // FFmpeg represents audio data marked as before the beginning of stream as |
| // having negative timestamps. This data must be discarded after it has been |
| // decoded, not before since it is used to warmup the decoder. There are |
| // currently two known cases for this: vorbis in ogg and opus in ogg and webm. |
| // |
| // For API clarity, it was decided that the rest of the media pipeline should |
| // not be exposed to negative timestamps. Which means we need to rebase these |
| // negative timestamps and mark them for discard post decoding. |
| // |
| // Post-decode frame dropping for packets with negative timestamps is outlined |
| // in section A.2 in the Ogg Vorbis spec: |
| // http://xiph.org/vorbis/doc/Vorbis_I_spec.html |
| // |
| // FFmpeg's use of negative timestamps for opus pre-skip is nonstandard, but |
| // for more information on pre-skip see section 4.2 of the Ogg Opus spec: |
| // https://tools.ietf.org/html/draft-ietf-codec-oggopus-08#section-4.2 |
| for (const auto& stream : streams_) { |
| if (!stream || stream->type() != DemuxerStream::AUDIO) |
| continue; |
| const AVStream* audio_stream = stream->av_stream(); |
| DCHECK(audio_stream); |
| if (audio_stream->codecpar->codec_id == AV_CODEC_ID_OPUS || |
| (strcmp(format_context->iformat->name, "ogg") == 0 && |
| audio_stream->codecpar->codec_id == AV_CODEC_ID_VORBIS)) { |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| if (!streams_[i]) |
| continue; |
| streams_[i]->enable_negative_timestamp_fixups(); |
| |
| // Fixup the seeking information to avoid selecting the audio stream |
| // simply because it has a lower starting time. |
| if (streams_[i]->av_stream() == audio_stream && |
| streams_[i]->start_time() < base::TimeDelta()) { |
| streams_[i]->set_start_time(base::TimeDelta()); |
| } |
| } |
| } |
| } |
| |
| // If no start time could be determined, default to zero. |
| if (start_time_ == kInfiniteDuration) { |
| start_time_ = base::TimeDelta(); |
| } |
| |
| // MPEG-4 B-frames cause grief for a simple container like AVI. Enable PTS |
| // generation so we always get timestamps, see http://crbug.com/169570 |
| if (strcmp(format_context->iformat->name, "avi") == 0) |
| format_context->flags |= AVFMT_FLAG_GENPTS; |
| |
| // For testing purposes, don't overwrite the timeline offset if set already. |
| if (timeline_offset_.is_null()) |
| timeline_offset_ = ExtractTimelineOffset(format_context); |
| |
| // Since we're shifting the externally visible start time to zero, we need to |
| // adjust the timeline offset to compensate. |
| if (!timeline_offset_.is_null() && start_time_ < base::TimeDelta()) |
| timeline_offset_ += start_time_; |
| |
| if (max_duration == kInfiniteDuration && !timeline_offset_.is_null()) { |
| SetLiveness(DemuxerStream::LIVENESS_LIVE); |
| } else if (max_duration != kInfiniteDuration) { |
| SetLiveness(DemuxerStream::LIVENESS_RECORDED); |
| } else { |
| SetLiveness(DemuxerStream::LIVENESS_UNKNOWN); |
| } |
| |
| // Good to go: set the duration and bitrate and notify we're done |
| // initializing. |
| host_->SetDuration(max_duration); |
| duration_known_ = (max_duration != kInfiniteDuration); |
| |
| int64_t filesize_in_bytes = 0; |
| url_protocol_->GetSize(&filesize_in_bytes); |
| bitrate_ = CalculateBitrate(format_context, max_duration, filesize_in_bytes); |
| if (bitrate_ > 0) |
| data_source_->SetBitrate(bitrate_); |
| |
| LogMetadata(format_context, max_duration); |
| media_tracks_updated_cb_.Run(std::move(media_tracks)); |
| |
| status_cb.Run(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::LogMetadata(AVFormatContext* avctx, |
| base::TimeDelta max_duration) { |
| // Use a single MediaLogEvent to batch all parameter updates at once; this |
| // prevents throttling of events due to the large number of updates here. |
| std::unique_ptr<MediaLogEvent> metadata_event = |
| media_log_->CreateEvent(MediaLogEvent::PROPERTY_CHANGE); |
| |
| DCHECK_EQ(avctx->nb_streams, streams_.size()); |
| auto& params = metadata_event->params; |
| int audio_track_count = 0; |
| int video_track_count = 0; |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| FFmpegDemuxerStream* stream = streams_[i].get(); |
| if (!stream) |
| continue; |
| if (stream->type() == DemuxerStream::AUDIO) { |
| ++audio_track_count; |
| std::string suffix = ""; |
| if (audio_track_count > 1) |
| suffix = "_track" + base::IntToString(audio_track_count); |
| const AVCodecParameters* audio_parameters = avctx->streams[i]->codecpar; |
| const AudioDecoderConfig& audio_config = stream->audio_decoder_config(); |
| params.SetString("audio_codec_name" + suffix, |
| GetCodecName(audio_parameters->codec_id)); |
| params.SetInteger("audio_channels_count" + suffix, |
| audio_parameters->channels); |
| params.SetString("audio_sample_format" + suffix, |
| SampleFormatToString(audio_config.sample_format())); |
| params.SetInteger("audio_samples_per_second" + suffix, |
| audio_config.samples_per_second()); |
| } else if (stream->type() == DemuxerStream::VIDEO) { |
| ++video_track_count; |
| std::string suffix = ""; |
| if (video_track_count > 1) |
| suffix = "_track" + base::IntToString(video_track_count); |
| const AVStream* video_av_stream = avctx->streams[i]; |
| const AVCodecParameters* video_parameters = video_av_stream->codecpar; |
| const VideoDecoderConfig& video_config = stream->video_decoder_config(); |
| params.SetString("video_codec_name" + suffix, |
| GetCodecName(video_parameters->codec_id)); |
| params.SetInteger("width" + suffix, video_parameters->width); |
| params.SetInteger("height" + suffix, video_parameters->height); |
| |
| // AVCodecParameters has no time_base field. We use the one from AVStream |
| // here. |
| params.SetString( |
| "time_base" + suffix, |
| base::StringPrintf("%d/%d", video_av_stream->time_base.num, |
| video_av_stream->time_base.den)); |
| |
| params.SetString("video_format" + suffix, |
| VideoPixelFormatToString(video_config.format())); |
| params.SetBoolean("video_is_encrypted" + suffix, |
| video_config.is_encrypted()); |
| } |
| } |
| params.SetBoolean("found_audio_stream", (audio_track_count > 0)); |
| params.SetBoolean("found_video_stream", (video_track_count > 0)); |
| SetTimeProperty(metadata_event.get(), "max_duration", max_duration); |
| SetTimeProperty(metadata_event.get(), "start_time", start_time_); |
| metadata_event->params.SetInteger("bitrate", bitrate_); |
| media_log_->AddEvent(std::move(metadata_event)); |
| } |
| |
| FFmpegDemuxerStream* FFmpegDemuxer::FindPreferredStreamForSeeking( |
| base::TimeDelta seek_time) { |
| // If we have a selected/enabled video stream and its start time is lower |
| // than the |seek_time| or unknown, then always prefer it for seeking. |
| FFmpegDemuxerStream* video_stream = nullptr; |
| for (const auto& stream : streams_) { |
| if (stream && stream->type() == DemuxerStream::VIDEO && stream->enabled()) { |
| video_stream = stream.get(); |
| if (video_stream->start_time() == kNoTimestamp || |
| video_stream->start_time() <= seek_time) { |
| return video_stream; |
| } |
| break; |
| } |
| } |
| |
| // If video stream is not present or |seek_time| is lower than the video start |
| // time, then try to find an enabled stream with the lowest start time. |
| FFmpegDemuxerStream* lowest_start_time_stream = nullptr; |
| for (const auto& stream : streams_) { |
| if (!stream || !stream->enabled() || stream->start_time() == kNoTimestamp) |
| continue; |
| if (!lowest_start_time_stream || |
| stream->start_time() < lowest_start_time_stream->start_time()) { |
| lowest_start_time_stream = stream.get(); |
| } |
| } |
| // If we found a stream with start time lower than |seek_time|, then use it. |
| if (lowest_start_time_stream && |
| lowest_start_time_stream->start_time() <= seek_time) { |
| return lowest_start_time_stream; |
| } |
| |
| // If we couldn't find any streams with the start time lower than |seek_time| |
| // then use either video (if one exists) or any audio stream. |
| return video_stream ? video_stream : static_cast<FFmpegDemuxerStream*>( |
| GetStream(DemuxerStream::AUDIO)); |
| } |
| |
| void FFmpegDemuxer::OnSeekFrameDone(int result) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| CHECK(!pending_seek_cb_.is_null()); |
| |
| if (!blocking_thread_.IsRunning()) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| base::ResetAndReturn(&pending_seek_cb_).Run(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| if (result < 0) { |
| // Use VLOG(1) instead of NOTIMPLEMENTED() to prevent the message being |
| // captured from stdout and contaminates testing. |
| // TODO(scherkus): Implement this properly and signal error (BUG=23447). |
| VLOG(1) << "Not implemented"; |
| } |
| |
| // Tell streams to flush buffers due to seeking. |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->FlushBuffers(); |
| } |
| |
| // Resume reading until capacity. |
| ReadFrameIfNeeded(); |
| |
| // Notify we're finished seeking. |
| base::ResetAndReturn(&pending_seek_cb_).Run(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::OnEnabledAudioTracksChanged( |
| const std::vector<MediaTrack::Id>& track_ids, |
| base::TimeDelta currTime) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| std::set<DemuxerStream*> enabled_streams; |
| for (const auto& id : track_ids) { |
| DemuxerStream* stream = track_id_to_demux_stream_map_[id]; |
| DCHECK(stream); |
| DCHECK_EQ(DemuxerStream::AUDIO, stream->type()); |
| enabled_streams.insert(stream); |
| } |
| |
| // First disable all streams that need to be disabled and then enable streams |
| // that are enabled. |
| for (const auto& stream : streams_) { |
| if (stream->type() == DemuxerStream::AUDIO && |
| enabled_streams.find(stream.get()) == enabled_streams.end()) { |
| DVLOG(1) << __func__ << ": disabling stream " << stream.get(); |
| stream->set_enabled(false, currTime); |
| } |
| } |
| for (const auto& stream : enabled_streams) { |
| DVLOG(1) << __func__ << ": enabling stream " << stream; |
| stream->set_enabled(true, currTime); |
| } |
| } |
| |
| void FFmpegDemuxer::OnSelectedVideoTrackChanged( |
| const std::vector<MediaTrack::Id>& track_ids, |
| base::TimeDelta currTime) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_LE(track_ids.size(), 1u); |
| |
| DemuxerStream* selected_stream = nullptr; |
| if (!track_ids.empty()) { |
| selected_stream = track_id_to_demux_stream_map_[track_ids[0]]; |
| DCHECK(selected_stream); |
| DCHECK_EQ(DemuxerStream::VIDEO, selected_stream->type()); |
| } |
| |
| // First disable all streams that need to be disabled and then enable the |
| // stream that needs to be enabled (if any). |
| for (const auto& stream : streams_) { |
| if (stream->type() == DemuxerStream::VIDEO && |
| stream.get() != selected_stream) { |
| DVLOG(1) << __func__ << ": disabling stream " << stream.get(); |
| stream->set_enabled(false, currTime); |
| } |
| } |
| if (selected_stream) { |
| DVLOG(1) << __func__ << ": enabling stream " << selected_stream; |
| selected_stream->set_enabled(true, currTime); |
| } |
| } |
| |
| void FFmpegDemuxer::ReadFrameIfNeeded() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| // Make sure we have work to do before reading. |
| if (!blocking_thread_.IsRunning() || !StreamsHaveAvailableCapacity() || |
| pending_read_ || !pending_seek_cb_.is_null()) { |
| return; |
| } |
| |
| // Allocate and read an AVPacket from the media. Save |packet_ptr| since |
| // evaluation order of packet.get() and base::Passed(&packet) is |
| // undefined. |
| ScopedAVPacket packet(new AVPacket()); |
| AVPacket* packet_ptr = packet.get(); |
| |
| pending_read_ = true; |
| base::PostTaskAndReplyWithResult( |
| blocking_thread_.task_runner().get(), |
| FROM_HERE, |
| base::Bind(&av_read_frame, glue_->format_context(), packet_ptr), |
| base::Bind(&FFmpegDemuxer::OnReadFrameDone, |
| weak_factory_.GetWeakPtr(), |
| base::Passed(&packet))); |
| } |
| |
| void FFmpegDemuxer::OnReadFrameDone(ScopedAVPacket packet, int result) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(pending_read_); |
| pending_read_ = false; |
| |
| if (!blocking_thread_.IsRunning() || !pending_seek_cb_.is_null()) |
| return; |
| |
| // Consider the stream as ended if: |
| // - either underlying ffmpeg returned an error |
| // - or FFMpegDemuxer reached the maximum allowed memory usage. |
| if (result < 0 || IsMaxMemoryUsageReached()) { |
| DVLOG(1) << __func__ << " result=" << result |
| << " IsMaxMemoryUsageReached=" << IsMaxMemoryUsageReached(); |
| // Update the duration based on the highest elapsed time across all streams |
| // if it was previously unknown. |
| if (!duration_known_) { |
| base::TimeDelta max_duration; |
| |
| for (const auto& stream : streams_) { |
| if (!stream) |
| continue; |
| |
| base::TimeDelta duration = stream->GetElapsedTime(); |
| if (duration != kNoTimestamp && duration > max_duration) |
| max_duration = duration; |
| } |
| |
| if (max_duration > base::TimeDelta()) { |
| host_->SetDuration(max_duration); |
| duration_known_ = true; |
| } |
| } |
| // If we have reached the end of stream, tell the downstream filters about |
| // the event. |
| StreamHasEnded(); |
| return; |
| } |
| |
| // Queue the packet with the appropriate stream. |
| DCHECK_GE(packet->stream_index, 0); |
| DCHECK_LT(packet->stream_index, static_cast<int>(streams_.size())); |
| |
| // Defend against ffmpeg giving us a bad stream index. |
| if (packet->stream_index >= 0 && |
| packet->stream_index < static_cast<int>(streams_.size()) && |
| streams_[packet->stream_index]) { |
| // TODO(scherkus): Fix demuxing upstream to never return packets w/o data |
| // when av_read_frame() returns success code. See bug comment for ideas: |
| // |
| // https://code.google.com/p/chromium/issues/detail?id=169133#c10 |
| if (!packet->data) { |
| ScopedAVPacket new_packet(new AVPacket()); |
| av_new_packet(new_packet.get(), 0); |
| av_packet_copy_props(new_packet.get(), packet.get()); |
| packet.swap(new_packet); |
| } |
| |
| FFmpegDemuxerStream* demuxer_stream = streams_[packet->stream_index].get(); |
| if (demuxer_stream->enabled()) |
| demuxer_stream->EnqueuePacket(std::move(packet)); |
| } |
| |
| // Keep reading until we've reached capacity. |
| ReadFrameIfNeeded(); |
| } |
| |
| bool FFmpegDemuxer::StreamsHaveAvailableCapacity() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| for (const auto& stream : streams_) { |
| if (stream && stream->HasAvailableCapacity()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool FFmpegDemuxer::IsMaxMemoryUsageReached() const { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| // Max allowed memory usage, all streams combined. |
| const size_t kDemuxerMemoryLimit = 150 * 1024 * 1024; |
| |
| size_t memory_left = kDemuxerMemoryLimit; |
| for (const auto& stream : streams_) { |
| if (!stream) |
| continue; |
| |
| size_t stream_memory_usage = stream->MemoryUsage(); |
| if (stream_memory_usage > memory_left) |
| return true; |
| memory_left -= stream_memory_usage; |
| } |
| return false; |
| } |
| |
| void FFmpegDemuxer::StreamHasEnded() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->SetEndOfStream(); |
| } |
| } |
| |
| void FFmpegDemuxer::OnDataSourceError() { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": data source error"; |
| host_->OnDemuxerError(PIPELINE_ERROR_READ); |
| } |
| |
| void FFmpegDemuxer::SetLiveness(DemuxerStream::Liveness liveness) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->SetLiveness(liveness); |
| } |
| } |
| |
| } // namespace media |