| // 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/feature_list.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/sys_byteorder.h" |
| #include "base/task/post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/demuxer_memory_limit.h" |
| #include "media/base/limits.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/media_tracks.h" |
| #include "media/base/media_types.h" |
| #include "media/base/sample_rates.h" |
| #include "media/base/supported_types.h" |
| #include "media/base/timestamp_constants.h" |
| #include "media/base/video_codecs.h" |
| #include "media/base/webvtt_util.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/formats/mpeg/mpeg1_audio_stream_parser.h" |
| #include "media/formats/webm/webm_crypto_helpers.h" |
| #include "media/media_buildflags.h" |
| #include "third_party/ffmpeg/ffmpeg_features.h" |
| #include "third_party/ffmpeg/libavcodec/packet.h" |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_HEVC) |
| #include "media/filters/ffmpeg_h265_to_annex_b_bitstream_converter.h" |
| #endif |
| |
| namespace media { |
| |
| namespace { |
| |
| void SetAVStreamDiscard(AVStream* stream, AVDiscard discard) { |
| DCHECK(stream); |
| stream->discard = discard; |
| } |
| |
| } // namespace |
| |
| ScopedAVPacket MakeScopedAVPacket() { |
| ScopedAVPacket packet(av_packet_alloc()); |
| return packet; |
| } |
| |
| static base::Time ExtractTimelineOffset( |
| container_names::MediaContainerName container, |
| const AVFormatContext* format_context) { |
| if (container == container_names::CONTAINER_WEBM) { |
| const AVDictionaryEntry* entry = |
| av_dict_get(format_context->metadata, "creation_time", nullptr, 0); |
| |
| base::Time timeline_offset; |
| |
| // FFmpegDemuxerTests assume base::Time::FromUTCString() is used here. |
| if (entry != nullptr && entry->value != nullptr && |
| base::Time::FromUTCString(entry->value, &timeline_offset)) { |
| return timeline_offset; |
| } |
| } |
| |
| return base::Time(); |
| } |
| |
| static base::TimeDelta FramesToTimeDelta(int frames, double sample_rate) { |
| return base::Microseconds(frames * base::Time::kMicrosecondsPerSecond / |
| sample_rate); |
| } |
| |
| static base::TimeDelta ExtractStartTime(AVStream* stream) { |
| // The default start time is zero. |
| base::TimeDelta start_time; |
| |
| // First try to use the |start_time| value as is. |
| if (stream->start_time != kNoFFmpegTimestamp) |
| start_time = ConvertFromTimeBase(stream->time_base, stream->start_time); |
| |
| // Next try to use the first DTS value, for codecs where we know PTS == DTS |
| // (excludes all H26x codecs). The start time must be returned in PTS. |
| if (av_stream_get_first_dts(stream) != kNoFFmpegTimestamp && |
| stream->codecpar->codec_id != AV_CODEC_ID_HEVC && |
| stream->codecpar->codec_id != AV_CODEC_ID_H264 && |
| stream->codecpar->codec_id != AV_CODEC_ID_MPEG4) { |
| const base::TimeDelta first_pts = |
| ConvertFromTimeBase(stream->time_base, av_stream_get_first_dts(stream)); |
| if (first_pts < start_time) |
| start_time = first_pts; |
| } |
| |
| return start_time; |
| } |
| |
| // Record audio decoder config UMA stats corresponding to a src= playback. |
| static void RecordAudioCodecStats(const AudioDecoderConfig& audio_config) { |
| base::UmaHistogramEnumeration("Media.AudioCodec", audio_config.codec()); |
| } |
| |
| // Record video decoder config UMA stats corresponding to a src= playback. |
| static void RecordVideoCodecStats(container_names::MediaContainerName container, |
| const VideoDecoderConfig& video_config, |
| AVColorRange color_range, |
| MediaLog* media_log) { |
| // TODO(xhwang): Fix these misleading metric names. They should be something |
| // like "Media.SRC.Xxxx". See http://crbug.com/716183. |
| base::UmaHistogramEnumeration("Media.VideoCodec", video_config.codec()); |
| if (container == container_names::CONTAINER_MOV) { |
| base::UmaHistogramEnumeration("Media.SRC.VideoCodec.MP4", |
| video_config.codec()); |
| } else if (container == container_names::CONTAINER_WEBM) { |
| base::UmaHistogramEnumeration("Media.SRC.VideoCodec.WebM", |
| video_config.codec()); |
| } |
| } |
| |
| 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 base::Value GetTimeValue(base::TimeDelta value) { |
| if (value == kInfiniteDuration) |
| return base::Value("kInfiniteDuration"); |
| if (value == kNoTimestamp) |
| return base::Value("kNoTimestamp"); |
| return base::Value(value.InSecondsF()); |
| } |
| |
| template <> |
| struct MediaLogPropertyTypeSupport<MediaLogProperty::kMaxDuration, |
| base::TimeDelta> { |
| static base::Value Convert(base::TimeDelta t) { return GetTimeValue(t); } |
| }; |
| |
| template <> |
| struct MediaLogPropertyTypeSupport<MediaLogProperty::kStartTime, |
| base::TimeDelta> { |
| static base::Value Convert(base::TimeDelta t) { return GetTimeValue(t); } |
| }; |
| |
| static int ReadFrameAndDiscardEmpty(AVFormatContext* context, |
| AVPacket* packet) { |
| // Skip empty packets in a tight loop to avoid timing out fuzzers. |
| int result; |
| bool drop_packet; |
| do { |
| result = av_read_frame(context, packet); |
| drop_packet = (!packet->data || !packet->size) && result >= 0; |
| if (drop_packet) { |
| av_packet_unref(packet); |
| DLOG(WARNING) << "Dropping empty packet, size: " << packet->size |
| << ", data: " << static_cast<void*>(packet->data); |
| } |
| } while (drop_packet); |
| |
| return result; |
| } |
| |
| std::unique_ptr<FFmpegDemuxerStream> FFmpegDemuxerStream::Create( |
| FFmpegDemuxer* demuxer, |
| AVStream* stream, |
| 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 defined(OS_CHROMEOS) |
| if (base::FeatureList::IsEnabled(kDeprecateLowUsageCodecs)) { |
| const auto codec_id = stream->codecpar->codec_id; |
| if (codec_id == AV_CODEC_ID_AMR_NB || codec_id == AV_CODEC_ID_AMR_WB || |
| codec_id == AV_CODEC_ID_GSM_MS) { |
| MEDIA_LOG(ERROR, media_log) << "AMR and GSM are deprecated on ChromeOS."; |
| return nullptr; |
| } |
| } |
| #endif |
| |
| if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { |
| audio_config = std::make_unique<AudioDecoderConfig>(); |
| |
| // 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() || |
| !IsSupportedAudioType(AudioType::FromDecoderConfig(*audio_config))) { |
| MEDIA_LOG(DEBUG, media_log) << "Warning, FFmpegDemuxer failed to create " |
| "a valid/supported audio decoder " |
| "configuration from muxed stream, config:" |
| << audio_config->AsHumanReadableString(); |
| 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 = std::make_unique<VideoDecoderConfig>(); |
| |
| // 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() || |
| !IsSupportedVideoType(VideoType::FromDecoderConfig(*video_config))) { |
| MEDIA_LOG(DEBUG, media_log) << "Warning, FFmpegDemuxer failed to create " |
| "a valid/supported video decoder " |
| "configuration from muxed stream, config:" |
| << video_config->AsHumanReadableString(); |
| 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), media_log)); |
| } |
| |
| static void UnmarkEndOfStreamAndClearError(AVFormatContext* format_context) { |
| format_context->pb->eof_reached = 0; |
| format_context->pb->error = 0; |
| } |
| |
| // |
| // FFmpegDemuxerStream |
| // |
| FFmpegDemuxerStream::FFmpegDemuxerStream( |
| FFmpegDemuxer* demuxer, |
| AVStream* stream, |
| std::unique_ptr<AudioDecoderConfig> audio_config, |
| std::unique_ptr<VideoDecoderConfig> video_config, |
| MediaLog* media_log) |
| : demuxer_(demuxer), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| stream_(stream), |
| start_time_(kNoTimestamp), |
| audio_config_(audio_config.release()), |
| video_config_(video_config.release()), |
| media_log_(media_log), |
| type_(UNKNOWN), |
| liveness_(LIVENESS_UNKNOWN), |
| end_of_stream_(false), |
| last_packet_timestamp_(kNoTimestamp), |
| last_packet_duration_(kNoTimestamp), |
| is_enabled_(true), |
| waiting_for_keyframe_(false), |
| aborted_(false), |
| fixup_negative_timestamps_(false), |
| fixup_chained_ogg_(false), |
| num_discarded_packet_warnings_(0), |
| last_packet_pos_(AV_NOPTS_VALUE), |
| last_packet_dts_(AV_NOPTS_VALUE) { |
| DCHECK(demuxer_); |
| |
| bool is_encrypted = false; |
| |
| // 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(); |
| 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", nullptr, 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_); |
| DCHECK(buffer_queue_.IsEmpty()); |
| } |
| |
| void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(packet->size); |
| DCHECK(packet->data); |
| |
| const bool is_audio = type() == AUDIO; |
| |
| // dts == pts when dts is not present. |
| int64_t packet_dts = |
| packet->dts == AV_NOPTS_VALUE ? packet->pts : packet->dts; |
| |
| // Chained ogg files have non-monotonically increasing position and time stamp |
| // values, which prevents us from using them to determine if a packet should |
| // be dropped. Since chained ogg is only allowed on single track audio only |
| // opus/vorbis media, and dropping packets is only necessary for multi-track |
| // video-and-audio streams, we can just disable dropping when we detect |
| // chained ogg. |
| // For similar reasons, we only want to allow packet drops for audio streams; |
| // video frame dropping is handled by the renderer when correcting for a/v |
| // sync. |
| if (is_audio && !fixup_chained_ogg_ && last_packet_pos_ != AV_NOPTS_VALUE) { |
| // Some containers have unknown position... |
| if (packet->pos == -1) |
| packet->pos = last_packet_pos_; |
| |
| if (packet->pos < last_packet_pos_) { |
| DVLOG(3) << "Dropped packet with out of order position (" << packet->pos |
| << " < " << last_packet_pos_ << ")"; |
| return; |
| } |
| if (last_packet_dts_ != AV_NOPTS_VALUE && packet->pos == last_packet_pos_ && |
| packet_dts <= last_packet_dts_) { |
| DVLOG(3) << "Dropped packet with out of order display timestamp (" |
| << packet_dts << " < " << last_packet_dts_ << ")"; |
| return; |
| } |
| } |
| |
| if (!demuxer_ || end_of_stream_) { |
| NOTREACHED() << "Attempted to enqueue packet on a stopped stream"; |
| return; |
| } |
| |
| last_packet_pos_ = packet->pos; |
| last_packet_dts_ = packet_dts; |
| |
| if (waiting_for_keyframe_) { |
| if (packet->flags & AV_PKT_FLAG_KEY) { |
| waiting_for_keyframe_ = false; |
| } else { |
| DVLOG(1) << "Dropped non-keyframe pts=" << packet->pts; |
| return; |
| } |
| } |
| |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| // Convert the packet if there is a bitstream filter. |
| if (bitstream_converter_ && |
| !bitstream_converter_->ConvertPacket(packet.get())) { |
| DVLOG(1) << "Format conversion failed."; |
| } |
| #endif |
| |
| scoped_refptr<DecoderBuffer> buffer; |
| |
| if (type() == DemuxerStream::TEXT) { |
| size_t id_size = 0; |
| uint8_t* id_data = av_packet_get_side_data( |
| packet.get(), AV_PKT_DATA_WEBVTT_IDENTIFIER, &id_size); |
| |
| size_t 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->data, packet->size, |
| side_data.data(), side_data.size()); |
| } else { |
| size_t 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)) { |
| MEDIA_LOG(ERROR, media_log_) << "Creation of DecryptConfig failed."; |
| } |
| } |
| |
| // FFmpeg may return garbage packets for MP3 stream containers, so we need |
| // to drop these to avoid decoder errors. The ffmpeg team maintains that |
| // this behavior isn't ideal, but have asked for a significant refactoring |
| // of the AVParser infrastructure to fix this, which is overkill for now. |
| // See http://crbug.com/794782. |
| // |
| // This behavior may also occur with ADTS streams, but is rarer in practice |
| // because ffmpeg's ADTS demuxer does more validation on the packets, so |
| // when invalid data is received, av_read_frame() fails and playback ends. |
| if (is_audio && demuxer_->container() == container_names::CONTAINER_MP3) { |
| DCHECK(!data_offset); // Only set for containers supporting encryption... |
| |
| // MP3 packets may be zero-padded according to ffmpeg, so trim until we |
| // have the packet; adjust |data_offset| too so this work isn't repeated. |
| uint8_t* packet_end = packet->data + packet->size; |
| uint8_t* header_start = packet->data; |
| while (header_start < packet_end && !*header_start) { |
| ++header_start; |
| ++data_offset; |
| } |
| |
| if (packet_end - header_start < MPEG1AudioStreamParser::kHeaderSize || |
| !MPEG1AudioStreamParser::ParseHeader(nullptr, nullptr, header_start, |
| nullptr)) { |
| LIMITED_MEDIA_LOG(INFO, media_log_, num_discarded_packet_warnings_, 5) |
| << "Discarding invalid MP3 packet, ts: " |
| << ConvertStreamTimestamp(stream_->time_base, packet->pts) |
| << ", duration: " |
| << ConvertStreamTimestamp(stream_->time_base, packet->duration); |
| return; |
| } |
| } |
| |
| // 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->data + data_offset, |
| packet->size - data_offset, side_data, |
| side_data_size); |
| } else { |
| buffer = DecoderBuffer::CopyFrom(packet->data + data_offset, |
| packet->size - data_offset); |
| } |
| |
| size_t 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)); |
| |
| if (discard_front_samples || discard_end_samples) { |
| DCHECK(is_audio); |
| 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 kNoFFmpegTimestamp, stream_timestamp will be kNoTimestamp. |
| const base::TimeDelta stream_timestamp = |
| ConvertStreamTimestamp(stream_->time_base, packet->pts); |
| |
| if (stream_timestamp == kNoTimestamp || |
| stream_timestamp == kInfiniteDuration) { |
| MEDIA_LOG(ERROR, media_log_) << "FFmpegDemuxer: PTS is not defined"; |
| demuxer_->NotifyDemuxerError(DEMUXER_ERROR_COULD_NOT_PARSE); |
| return; |
| } |
| |
| // 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 the packet is marked for complete discard and it doesn't already have |
| // any discard padding set, mark the DecoderBuffer for complete discard. We |
| // don't want to overwrite any existing discard padding since the discard |
| // padding may refer to frames beyond this packet. |
| if (packet->flags & AV_PKT_FLAG_DISCARD && |
| buffer->discard_padding() == DecoderBuffer::DiscardPadding()) { |
| buffer->set_discard_padding( |
| std::make_pair(kInfiniteDuration, base::TimeDelta())); |
| // These timestamps should never be used, but to ensure they are dropped |
| // correctly give them unique timestamps. |
| buffer->set_timestamp(last_packet_timestamp_ == kNoTimestamp |
| ? base::TimeDelta() |
| : last_packet_timestamp_ + base::Microseconds(1)); |
| } |
| |
| // Fixup negative timestamps where the before-zero portion is completely |
| // discarded after decoding. |
| if (buffer->timestamp() < base::TimeDelta()) { |
| // Discard padding may also remove samples after zero. |
| auto fixed_ts = buffer->discard_padding().first + buffer->timestamp(); |
| |
| // Allow for rounding error in the discard padding calculations. |
| if (fixed_ts == base::Microseconds(-1)) |
| fixed_ts = base::TimeDelta(); |
| |
| if (fixed_ts >= base::TimeDelta()) |
| buffer->set_timestamp(fixed_ts); |
| } |
| |
| // Only allow negative timestamps past if we know they'll be fixed up by the |
| // code paths below; otherwise they should be treated as a parse error. |
| if ((!fixup_chained_ogg_ || last_packet_timestamp_ == kNoTimestamp) && |
| buffer->timestamp() < base::TimeDelta()) { |
| MEDIA_LOG(ERROR, media_log_) |
| << "FFmpegDemuxer: unfixable negative timestamp."; |
| demuxer_->NotifyDemuxerError(DEMUXER_ERROR_COULD_NOT_PARSE); |
| return; |
| } |
| |
| // If enabled, and no codec delay is present, mark audio packets with negative |
| // timestamps for post-decode discard. If codec delay is present, discard is |
| // handled by the decoder using that value. |
| if (fixup_negative_timestamps_ && is_audio && |
| stream_timestamp < base::TimeDelta() && |
| buffer->duration() != kNoTimestamp && |
| !audio_decoder_config().codec_delay()) { |
| |
| 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, but don't |
| // override the discard padding if it refers to frames beyond this packet. |
| if (buffer->discard_padding().first <= buffer->duration()) { |
| 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( |
| std::max(-stream_timestamp, buffer->discard_padding().first), |
| buffer->discard_padding().second)); |
| } |
| } |
| |
| 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. |
| // |
| // 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. |
| // |
| // Note: This will not work with codecs that have out of order frames like |
| // H.264 with b-frames, but luckily you can't put those in ogg files... |
| if (fixup_chained_ogg_ && buffer->timestamp() < last_packet_timestamp_) { |
| buffer->set_timestamp(last_packet_timestamp_ + |
| (last_packet_duration_ != kNoTimestamp |
| ? last_packet_duration_ |
| : base::Microseconds(1))); |
| } |
| |
| // The demuxer should always output positive timestamps. |
| DCHECK_GE(buffer->timestamp(), base::TimeDelta()); |
| |
| if (last_packet_timestamp_ < buffer->timestamp()) { |
| buffered_ranges_.Add(last_packet_timestamp_, buffer->timestamp()); |
| demuxer_->NotifyBufferingChanged(); |
| } |
| } |
| |
| if (packet->flags & AV_PKT_FLAG_KEY) |
| buffer->set_is_key_frame(true); |
| |
| // One last sanity check on the packet timestamps in case any of the above |
| // calculations have pushed the values to the limits. |
| if (buffer->timestamp() == kNoTimestamp || |
| buffer->timestamp() == kInfiniteDuration) { |
| MEDIA_LOG(ERROR, media_log_) << "FFmpegDemuxer: PTS is not defined"; |
| demuxer_->NotifyDemuxerError(DEMUXER_ERROR_COULD_NOT_PARSE); |
| return; |
| } |
| |
| last_packet_timestamp_ = buffer->timestamp(); |
| last_packet_duration_ = buffer->duration(); |
| |
| const base::TimeDelta new_duration = last_packet_timestamp_; |
| if (new_duration > duration_ || duration_ == kNoTimestamp) |
| duration_ = new_duration; |
| |
| buffer_queue_.Push(std::move(buffer)); |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::SetEndOfStream() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| end_of_stream_ = true; |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::FlushBuffers(bool preserve_packet_position) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(preserve_packet_position || !read_cb_) |
| << "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(); |
| |
| if (!preserve_packet_position) { |
| last_packet_pos_ = AV_NOPTS_VALUE; |
| last_packet_dts_ = AV_NOPTS_VALUE; |
| } |
| |
| buffer_queue_.Clear(); |
| end_of_stream_ = false; |
| last_packet_timestamp_ = kNoTimestamp; |
| last_packet_duration_ = kNoTimestamp; |
| aborted_ = false; |
| } |
| |
| void FFmpegDemuxerStream::Abort() { |
| aborted_ = true; |
| if (read_cb_) |
| std::move(read_cb_).Run(DemuxerStream::kAborted, nullptr); |
| } |
| |
| void FFmpegDemuxerStream::Stop() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| buffer_queue_.Clear(); |
| demuxer_ = nullptr; |
| stream_ = nullptr; |
| end_of_stream_ = true; |
| if (read_cb_) { |
| std::move(read_cb_).Run(DemuxerStream::kOk, |
| DecoderBuffer::CreateEOSBuffer()); |
| } |
| } |
| |
| DemuxerStream::Type FFmpegDemuxerStream::type() const { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| return type_; |
| } |
| |
| DemuxerStream::Liveness FFmpegDemuxerStream::liveness() const { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| return liveness_; |
| } |
| |
| void FFmpegDemuxerStream::Read(ReadCB read_cb) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| CHECK(!read_cb_) << "Overlapping reads are not supported"; |
| read_cb_ = BindToCurrentLoop(std::move(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_) { |
| std::move(read_cb_).Run(DemuxerStream::kOk, |
| DecoderBuffer::CreateEOSBuffer()); |
| return; |
| } |
| |
| if (!is_enabled_) { |
| DVLOG(1) << "Read from disabled stream, returning EOS"; |
| std::move(read_cb_).Run(kOk, DecoderBuffer::CreateEOSBuffer()); |
| return; |
| } |
| |
| if (aborted_) { |
| std::move(read_cb_).Run(kAborted, nullptr); |
| return; |
| } |
| |
| SatisfyPendingRead(); |
| } |
| |
| void FFmpegDemuxerStream::EnableBitstreamConverter() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| InitBitstreamConverter(); |
| #else |
| DLOG(ERROR) << "Proprietary codecs not enabled and stream requires bitstream " |
| "conversion. Playback will likely fail."; |
| #endif |
| } |
| |
| void FFmpegDemuxerStream::ResetBitstreamConverter() { |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| if (bitstream_converter_) |
| InitBitstreamConverter(); |
| #endif // BUILDFLAG(USE_PROPRIETARY_CODECS) |
| } |
| |
| void FFmpegDemuxerStream::InitBitstreamConverter() { |
| #if BUILDFLAG(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_ = |
| std::make_unique<FFmpegH264ToAnnexBBitstreamConverter>( |
| stream_->codecpar); |
| break; |
| #if BUILDFLAG(ENABLE_PLATFORM_HEVC) |
| case AV_CODEC_ID_HEVC: |
| bitstream_converter_.reset( |
| new FFmpegH265ToAnnexBBitstreamConverter(stream_->codecpar)); |
| break; |
| #endif |
| case AV_CODEC_ID_AAC: |
| // FFmpeg doesn't understand xHE-AAC profiles yet, which can't be put in |
| // ADTS anyways, so skip bitstream conversion when the profile is |
| // unknown. |
| if (audio_config_->profile() != AudioCodecProfile::kXHE_AAC) { |
| bitstream_converter_ = |
| std::make_unique<FFmpegAACBitstreamConverter>(stream_->codecpar); |
| } |
| break; |
| default: |
| break; |
| } |
| #endif // BUILDFLAG(USE_PROPRIETARY_CODECS) |
| } |
| |
| bool FFmpegDemuxerStream::SupportsConfigChanges() { return false; } |
| |
| AudioDecoderConfig FFmpegDemuxerStream::audio_decoder_config() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK_EQ(type_, AUDIO); |
| DCHECK(audio_config_.get()); |
| return *audio_config_; |
| } |
| |
| VideoDecoderConfig FFmpegDemuxerStream::video_decoder_config() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK_EQ(type_, VIDEO); |
| DCHECK(video_config_.get()); |
| return *video_config_; |
| } |
| |
| bool FFmpegDemuxerStream::IsEnabled() const { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| return is_enabled_; |
| } |
| |
| void FFmpegDemuxerStream::SetEnabled(bool enabled, base::TimeDelta timestamp) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(demuxer_); |
| DCHECK(demuxer_->ffmpeg_task_runner()); |
| if (enabled == is_enabled_) |
| return; |
| |
| is_enabled_ = enabled; |
| demuxer_->ffmpeg_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&SetAVStreamDiscard, av_stream(), |
| enabled ? AVDISCARD_DEFAULT : AVDISCARD_ALL)); |
| if (is_enabled_) { |
| waiting_for_keyframe_ = true; |
| } |
| if (!is_enabled_ && read_cb_) { |
| DVLOG(1) << "Read from disabled stream, returning EOS"; |
| std::move(read_cb_).Run(kOk, DecoderBuffer::CreateEOSBuffer()); |
| } |
| } |
| |
| void FFmpegDemuxerStream::SetLiveness(Liveness liveness) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK_EQ(liveness_, LIVENESS_UNKNOWN); |
| liveness_ = liveness; |
| } |
| |
| Ranges<base::TimeDelta> FFmpegDemuxerStream::GetBufferedRanges() const { |
| return buffered_ranges_; |
| } |
| |
| void FFmpegDemuxerStream::SatisfyPendingRead() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (read_cb_) { |
| if (!buffer_queue_.IsEmpty()) { |
| std::move(read_cb_).Run(DemuxerStream::kOk, buffer_queue_.Pop()); |
| } else if (end_of_stream_) { |
| std::move(read_cb_).Run(DemuxerStream::kOk, |
| DecoderBuffer::CreateEOSBuffer()); |
| } |
| } |
| |
| // Have capacity? Ask for more! |
| if (HasAvailableCapacity() && !end_of_stream_) { |
| demuxer_->NotifyCapacityAvailable(); |
| } |
| } |
| |
| bool FFmpegDemuxerStream::HasAvailableCapacity() { |
| // Try to have two second's worth of encoded data per stream. |
| const base::TimeDelta kCapacity = base::Seconds(2); |
| return buffer_queue_.IsEmpty() || buffer_queue_.Duration() < kCapacity; |
| } |
| |
| size_t FFmpegDemuxerStream::MemoryUsage() const { |
| return buffer_queue_.data_size(); |
| } |
| |
| std::string FFmpegDemuxerStream::GetMetadata(const char* key) const { |
| const AVDictionaryEntry* entry = |
| av_dict_get(stream_->metadata, key, nullptr, 0); |
| return (entry == nullptr || entry->value == nullptr) ? "" : entry->value; |
| } |
| |
| // static |
| base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp( |
| const AVRational& time_base, |
| int64_t timestamp) { |
| if (timestamp == kNoFFmpegTimestamp) |
| return kNoTimestamp; |
| |
| return ConvertFromTimeBase(time_base, timestamp); |
| } |
| |
| // |
| // FFmpegDemuxer |
| // |
| FFmpegDemuxer::FFmpegDemuxer( |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner, |
| DataSource* data_source, |
| const EncryptedMediaInitDataCB& encrypted_media_init_data_cb, |
| MediaTracksUpdatedCB media_tracks_updated_cb, |
| MediaLog* media_log, |
| bool is_local_file) |
| : task_runner_(task_runner), |
| // FFmpeg has no asynchronous API, so we use base::WaitableEvents inside |
| // the BlockingUrlProtocol to handle hops to the render thread for network |
| // reads and seeks. |
| blocking_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING})), |
| data_source_(data_source), |
| media_log_(media_log), |
| encrypted_media_init_data_cb_(encrypted_media_init_data_cb), |
| media_tracks_updated_cb_(std::move(media_tracks_updated_cb)), |
| is_local_file_(is_local_file) { |
| DCHECK(task_runner_.get()); |
| DCHECK(data_source_); |
| DCHECK(media_tracks_updated_cb_); |
| } |
| |
| FFmpegDemuxer::~FFmpegDemuxer() { |
| DCHECK(!init_cb_); |
| DCHECK(!pending_seek_cb_); |
| |
| // 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()); |
| |
| // There may be outstanding tasks in the blocking pool which are trying to use |
| // these members, so release them in sequence with any outstanding calls. The |
| // earlier call to Abort() on |data_source_| prevents further access to it. |
| blocking_task_runner_->DeleteSoon(FROM_HERE, url_protocol_.release()); |
| blocking_task_runner_->DeleteSoon(FROM_HERE, glue_.release()); |
| } |
| |
| std::string FFmpegDemuxer::GetDisplayName() const { |
| return "FFmpegDemuxer"; |
| } |
| |
| void FFmpegDemuxer::Initialize(DemuxerHost* host, |
| PipelineStatusCallback init_cb) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| host_ = host; |
| weak_this_ = cancel_pending_seek_factory_.GetWeakPtr(); |
| init_cb_ = std::move(init_cb); |
| |
| // Give a WeakPtr to BlockingUrlProtocol since we'll need to release it on the |
| // blocking thread pool. |
| url_protocol_ = std::make_unique<BlockingUrlProtocol>( |
| data_source_, BindToCurrentLoop(base::BindRepeating( |
| &FFmpegDemuxer::OnDataSourceError, weak_this_))); |
| glue_ = std::make_unique<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. |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&FFmpegGlue::OpenContext, base::Unretained(glue_.get()), |
| is_local_file_), |
| base::BindOnce(&FFmpegDemuxer::OnOpenContextDone, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void FFmpegDemuxer::AbortPendingReads() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| // If Stop() has been called, then drop this call. |
| if (stopped_) |
| 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_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&UnmarkEndOfStreamAndClearError, 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_) |
| RunPendingSeekCB(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::Stop() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| if (init_cb_) |
| RunInitCB(PIPELINE_ERROR_ABORT); |
| if (pending_seek_cb_) |
| RunPendingSeekCB(PIPELINE_ERROR_ABORT); |
| |
| // 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(); |
| |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->Stop(); |
| } |
| |
| data_source_ = nullptr; |
| |
| // Invalidate WeakPtrs on |task_runner_|, destruction may happen on another |
| // thread. We don't need to wait for any outstanding tasks since they will all |
| // fail to return after invalidating WeakPtrs. |
| stopped_ = true; |
| 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_->RunsTasksInCurrentSequence()) { |
| AbortPendingReads(); |
| } else { |
| // Don't use GetWeakPtr() here since we are on the wrong thread. |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FFmpegDemuxer::AbortPendingReads, weak_this_)); |
| } |
| } |
| |
| void FFmpegDemuxer::Seek(base::TimeDelta time, PipelineStatusCallback cb) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(!pending_seek_cb_); |
| TRACE_EVENT_ASYNC_BEGIN0("media", "FFmpegDemuxer::Seek", this); |
| pending_seek_cb_ = std::move(cb); |
| SeekInternal(time, base::BindOnce(&FFmpegDemuxer::OnSeekFrameSuccess, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void FFmpegDemuxer::SeekInternal(base::TimeDelta time, |
| base::OnceClosure seek_cb) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| // 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; |
| if (start_time_ < base::TimeDelta()) { |
| seek_time = time + start_time_; |
| } else { |
| seek_time = std::max(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 = |
| GetFirstEnabledFFmpegStream(DemuxerStream::AUDIO); |
| if (audio_stream) { |
| const AudioDecoderConfig& config = audio_stream->audio_decoder_config(); |
| if (config.codec() == AudioCodec::kOpus) |
| 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); |
| |
| blocking_task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce(base::IgnoreResult(&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), |
| std::move(seek_cb)); |
| } |
| |
| base::Time FFmpegDemuxer::GetTimelineOffset() const { |
| return timeline_offset_; |
| } |
| |
| std::vector<DemuxerStream*> FFmpegDemuxer::GetAllStreams() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| std::vector<DemuxerStream*> result; |
| // Put enabled streams at the beginning of the list so that |
| // MediaResource::GetFirstStream returns the enabled stream if there is one. |
| // TODO(servolk): Revisit this after media track switching is supported. |
| for (const auto& stream : streams_) { |
| if (stream && stream->IsEnabled()) |
| result.push_back(stream.get()); |
| } |
| // And include disabled streams at the end of the list. |
| for (const auto& stream : streams_) { |
| if (stream && !stream->IsEnabled()) |
| result.push_back(stream.get()); |
| } |
| return result; |
| } |
| |
| FFmpegDemuxerStream* FFmpegDemuxer::GetFirstEnabledFFmpegStream( |
| DemuxerStream::Type type) const { |
| for (const auto& stream : streams_) { |
| if (stream && stream->type() == type && stream->IsEnabled()) { |
| return stream.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| base::TimeDelta FFmpegDemuxer::GetStartTime() const { |
| return std::max(start_time_, base::TimeDelta()); |
| } |
| |
| int64_t FFmpegDemuxer::GetMemoryUsage() const { |
| int64_t allocation_size = 0; |
| for (const auto& stream : streams_) { |
| if (stream) |
| allocation_size += stream->MemoryUsage(); |
| } |
| return allocation_size; |
| } |
| |
| absl::optional<container_names::MediaContainerName> |
| FFmpegDemuxer::GetContainerForMetrics() const { |
| return container(); |
| } |
| |
| 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_->RunsTasksInCurrentSequence()); |
| ReadFrameIfNeeded(); |
| } |
| |
| void FFmpegDemuxer::NotifyBufferingChanged() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| Ranges<base::TimeDelta> buffered; |
| bool initialized_buffered_ranges = false; |
| for (const auto& stream : streams_) { |
| if (!stream) |
| continue; |
| if (initialized_buffered_ranges) { |
| buffered = buffered.IntersectionWith(stream->GetBufferedRanges()); |
| } else { |
| buffered = stream->GetBufferedRanges(); |
| initialized_buffered_ranges = true; |
| } |
| } |
| 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 <= base::TimeDelta() || duration == kInfiniteDuration || |
| !filesize_in_bytes) |
| return 0; |
| |
| // Don't multiply by 8 first; it will overflow if (filesize_in_bytes >= 2^60). |
| return base::ClampRound(filesize_in_bytes * duration.ToHz() * 8); |
| } |
| |
| void FFmpegDemuxer::OnOpenContextDone(bool result) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (stopped_) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| RunInitCB(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| #if defined(OS_ANDROID) |
| if (glue_->detected_hls()) { |
| MEDIA_LOG(INFO, media_log_) |
| << GetDisplayName() << ": detected HLS manifest"; |
| RunInitCB(DEMUXER_ERROR_DETECTED_HLS); |
| return; |
| } |
| #endif |
| |
| if (!result) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": open context failed"; |
| RunInitCB(DEMUXER_ERROR_COULD_NOT_OPEN); |
| return; |
| } |
| |
| // Fully initialize AVFormatContext by parsing the stream a little. |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&avformat_find_stream_info, glue_->format_context(), |
| static_cast<AVDictionary**>(nullptr)), |
| base::BindOnce(&FFmpegDemuxer::OnFindStreamInfoDone, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void FFmpegDemuxer::OnFindStreamInfoDone(int result) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (stopped_ || !data_source_) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| RunInitCB(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| if (result < 0) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() |
| << ": find stream info failed"; |
| RunInitCB(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); |
| |
| 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 supported_audio_track_count = 0; |
| int supported_video_track_count = 0; |
| bool has_opus_or_vorbis_audio = false; |
| bool needs_negative_timestamp_fixup = false; |
| 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; |
| // Skip streams which are not properly detected. |
| if (codec_id == AV_CODEC_ID_NONE) { |
| stream->discard = AVDISCARD_ALL; |
| continue; |
| } |
| |
| 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. |
| const int32_t codec_hash = HashCodecName(GetCodecName(codec_id)); |
| base::UmaHistogramSparse("Media.DetectedAudioCodecHash", codec_hash); |
| if (is_local_file_) { |
| base::UmaHistogramSparse("Media.DetectedAudioCodecHash.Local", |
| codec_hash); |
| } |
| } 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. |
| const int32_t codec_hash = HashCodecName(GetCodecName(codec_id)); |
| base::UmaHistogramSparse("Media.DetectedVideoCodecHash", codec_hash); |
| if (is_local_file_) { |
| base::UmaHistogramSparse("Media.DetectedVideoCodecHash.Local", |
| codec_hash); |
| } |
| |
| #if BUILDFLAG(ENABLE_PLATFORM_HEVC) |
| 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) { |
| stream->discard = AVDISCARD_ALL; |
| continue; |
| } else { |
| stream->discard = AVDISCARD_ALL; |
| 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 = |
| static_cast<StreamParser::TrackId>(media_tracks->tracks().size() + 1); |
| auto track_label = |
| MediaTrack::Label(streams_[i]->GetMetadata("handler_name")); |
| auto track_language = |
| MediaTrack::Language(streams_[i]->GetMetadata("language")); |
| |
| // Some metadata is named differently in FFmpeg for webm files. |
| if (glue_->container() == container_names::CONTAINER_WEBM) |
| track_label = MediaTrack::Label(streams_[i]->GetMetadata("title")); |
| |
| if (codec_type == AVMEDIA_TYPE_AUDIO) { |
| ++supported_audio_track_count; |
| streams_[i]->SetEnabled(supported_audio_track_count == 1, |
| base::TimeDelta()); |
| } else if (codec_type == AVMEDIA_TYPE_VIDEO) { |
| ++supported_video_track_count; |
| streams_[i]->SetEnabled(supported_video_track_count == 1, |
| base::TimeDelta()); |
| } |
| |
| // TODO(chcunningham): Remove the IsValidConfig() checks below. If the |
| // config isn't valid we shouldn't have created a demuxer stream nor |
| // an entry in |media_tracks|, so the check should always be true. |
| 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, |
| MediaTrack::Kind("main"), |
| track_label, track_language); |
| media_track->set_id(MediaTrack::Id(base::NumberToString(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(glue_->container(), video_config, |
| stream->codecpar->color_range, media_log_); |
| |
| media_track = media_tracks->AddVideoTrack(video_config, track_id, |
| MediaTrack::Kind("main"), |
| track_label, track_language); |
| media_track->set_id(MediaTrack::Id(base::NumberToString(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()); |
| |
| base::TimeDelta start_time = ExtractStartTime(stream); |
| |
| // Note: This value is used for seeking, so we must take the true value and |
| // not the one possibly clamped to zero below. |
| if (start_time != kNoTimestamp && start_time < start_time_) |
| start_time_ = start_time; |
| |
| const bool is_opus_or_vorbis = |
| codec_id == AV_CODEC_ID_OPUS || codec_id == AV_CODEC_ID_VORBIS; |
| if (!has_opus_or_vorbis_audio) |
| has_opus_or_vorbis_audio = is_opus_or_vorbis; |
| |
| if (codec_type == AVMEDIA_TYPE_AUDIO && start_time < base::TimeDelta() && |
| is_opus_or_vorbis) { |
| needs_negative_timestamp_fixup = true; |
| |
| // Fixup the seeking information to avoid selecting the audio stream |
| // simply because it has a lower starting time. |
| start_time = base::TimeDelta(); |
| } |
| |
| streams_[i]->set_start_time(start_time); |
| } |
| |
| if (media_tracks->tracks().empty()) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() |
| << ": no supported streams"; |
| RunInitCB(DEMUXER_ERROR_NO_SUPPORTED_STREAMS); |
| return; |
| } |
| |
| if (format_context->duration != kNoFFmpegTimestamp) { |
| // 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; |
| } |
| |
| // Chained ogg is only allowed on single track audio only opus/vorbis media. |
| const bool needs_chained_ogg_fixup = |
| glue_->container() == container_names::CONTAINER_OGG && |
| supported_audio_track_count == 1 && !supported_video_track_count && |
| has_opus_or_vorbis_audio; |
| |
| // 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. |
| // |
| // 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 |
| if (needs_negative_timestamp_fixup || needs_chained_ogg_fixup) { |
| for (auto& stream : streams_) { |
| if (!stream) |
| continue; |
| if (needs_negative_timestamp_fixup) |
| stream->enable_negative_timestamp_fixups(); |
| if (needs_chained_ogg_fixup) |
| stream->enable_chained_ogg_fixups(); |
| } |
| } |
| |
| // 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 (glue_->container() == container_names::CONTAINER_AVI) |
| format_context->flags |= AVFMT_FLAG_GENPTS; |
| |
| // FFmpeg will incorrectly adjust the start time of MP3 files into the future |
| // based on discard samples. We were unable to fix this upstream without |
| // breaking ffmpeg functionality. https://crbug.com/1062037 |
| if (glue_->container() == container_names::CONTAINER_MP3) |
| start_time_ = base::TimeDelta(); |
| |
| // For testing purposes, don't overwrite the timeline offset if set already. |
| if (timeline_offset_.is_null()) { |
| timeline_offset_ = |
| ExtractTimelineOffset(glue_->container(), 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_ = 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)); |
| |
| RunInitCB(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::LogMetadata(AVFormatContext* avctx, |
| base::TimeDelta max_duration) { |
| std::vector<AudioDecoderConfig> audio_tracks; |
| std::vector<VideoDecoderConfig> video_tracks; |
| |
| DCHECK_EQ(avctx->nb_streams, streams_.size()); |
| |
| for (auto const& stream : streams_) { |
| if (!stream) |
| continue; |
| if (stream->type() == DemuxerStream::AUDIO) { |
| audio_tracks.push_back(stream->audio_decoder_config()); |
| } else if (stream->type() == DemuxerStream::VIDEO) { |
| video_tracks.push_back(stream->video_decoder_config()); |
| } |
| } |
| media_log_->SetProperty<MediaLogProperty::kAudioTracks>(audio_tracks); |
| media_log_->SetProperty<MediaLogProperty::kVideoTracks>(video_tracks); |
| media_log_->SetProperty<MediaLogProperty::kMaxDuration>(max_duration); |
| media_log_->SetProperty<MediaLogProperty::kStartTime>(start_time_); |
| media_log_->SetProperty<MediaLogProperty::kBitrate>(bitrate_); |
| } |
| |
| FFmpegDemuxerStream* FFmpegDemuxer::FindStreamWithLowestStartTimestamp( |
| bool enabled) { |
| FFmpegDemuxerStream* lowest_start_time_stream = nullptr; |
| for (const auto& stream : streams_) { |
| if (!stream || stream->IsEnabled() != enabled) |
| continue; |
| if (!lowest_start_time_stream || |
| stream->start_time() < lowest_start_time_stream->start_time()) { |
| lowest_start_time_stream = stream.get(); |
| } |
| } |
| return lowest_start_time_stream; |
| } |
| |
| 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->IsEnabled()) { |
| video_stream = stream.get(); |
| if (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_enabled_stream = |
| FindStreamWithLowestStartTimestamp(true); |
| if (lowest_start_time_enabled_stream && |
| lowest_start_time_enabled_stream->start_time() <= seek_time) { |
| return lowest_start_time_enabled_stream; |
| } |
| |
| // If there's no enabled streams to consider from, try a disabled stream with |
| // the lowest known start time. |
| FFmpegDemuxerStream* lowest_start_time_disabled_stream = |
| FindStreamWithLowestStartTimestamp(false); |
| if (lowest_start_time_disabled_stream && |
| lowest_start_time_disabled_stream->start_time() <= seek_time) { |
| return lowest_start_time_disabled_stream; |
| } |
| |
| // Otherwise fall back to any other stream. |
| for (const auto& stream : streams_) { |
| if (stream) |
| return stream.get(); |
| } |
| |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| void FFmpegDemuxer::OnSeekFrameSuccess() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(pending_seek_cb_); |
| |
| if (stopped_) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state"; |
| RunPendingSeekCB(PIPELINE_ERROR_ABORT); |
| return; |
| } |
| |
| // Tell streams to flush buffers due to seeking. |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->FlushBuffers(false); |
| } |
| |
| // Resume reading until capacity. |
| ReadFrameIfNeeded(); |
| |
| // Notify we're finished seeking. |
| RunPendingSeekCB(PIPELINE_OK); |
| } |
| |
| void FFmpegDemuxer::FindAndEnableProperTracks( |
| const std::vector<MediaTrack::Id>& track_ids, |
| base::TimeDelta curr_time, |
| DemuxerStream::Type track_type, |
| TrackChangeCB change_completed_cb) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| std::set<FFmpegDemuxerStream*> enabled_streams; |
| for (const auto& id : track_ids) { |
| auto it = track_id_to_demux_stream_map_.find(id); |
| if (it == track_id_to_demux_stream_map_.end()) |
| continue; |
| FFmpegDemuxerStream* stream = it->second; |
| DCHECK_EQ(track_type, stream->type()); |
| // TODO(servolk): Remove after multiple enabled audio tracks are supported |
| // by the media::RendererImpl. |
| if (!enabled_streams.empty()) { |
| MEDIA_LOG(INFO, media_log_) |
| << "Only one enabled audio track is supported, ignoring track " << id; |
| continue; |
| } |
| enabled_streams.insert(stream); |
| stream->SetEnabled(true, curr_time); |
| } |
| |
| // First disable all streams that need to be disabled and then enable streams |
| // that are enabled. |
| for (const auto& stream : streams_) { |
| if (stream && stream->type() == track_type && |
| enabled_streams.find(stream.get()) == enabled_streams.end()) { |
| DVLOG(1) << __func__ << ": disabling stream " << stream.get(); |
| stream->SetEnabled(false, curr_time); |
| } |
| } |
| |
| std::vector<DemuxerStream*> streams(enabled_streams.begin(), |
| enabled_streams.end()); |
| std::move(change_completed_cb).Run(track_type, streams); |
| } |
| |
| void FFmpegDemuxer::OnEnabledAudioTracksChanged( |
| const std::vector<MediaTrack::Id>& track_ids, |
| base::TimeDelta curr_time, |
| TrackChangeCB change_completed_cb) { |
| FindAndEnableProperTracks(track_ids, curr_time, DemuxerStream::AUDIO, |
| std::move(change_completed_cb)); |
| } |
| |
| void FFmpegDemuxer::OnVideoSeekedForTrackChange( |
| DemuxerStream* video_stream, |
| base::OnceClosure seek_completed_cb) { |
| static_cast<FFmpegDemuxerStream*>(video_stream)->FlushBuffers(true); |
| std::move(seek_completed_cb).Run(); |
| } |
| |
| void FFmpegDemuxer::SeekOnVideoTrackChange( |
| base::TimeDelta seek_to_time, |
| TrackChangeCB seek_completed_cb, |
| DemuxerStream::Type stream_type, |
| const std::vector<DemuxerStream*>& streams) { |
| DCHECK_EQ(stream_type, DemuxerStream::VIDEO); |
| if (streams.size() != 1u) { |
| // If FFmpegDemuxer::FindAndEnableProperTracks() was not able to find the |
| // selected streams in the ID->DemuxerStream map, then its possible for |
| // this vector to be empty. If that's the case, we don't want to bother |
| // with seeking, and just call the callback immediately. |
| std::move(seek_completed_cb).Run(stream_type, streams); |
| return; |
| } |
| SeekInternal(seek_to_time, |
| base::BindOnce(&FFmpegDemuxer::OnVideoSeekedForTrackChange, |
| weak_factory_.GetWeakPtr(), streams[0], |
| base::BindOnce(std::move(seek_completed_cb), |
| DemuxerStream::VIDEO, streams))); |
| } |
| |
| void FFmpegDemuxer::OnSelectedVideoTrackChanged( |
| const std::vector<MediaTrack::Id>& track_ids, |
| base::TimeDelta curr_time, |
| TrackChangeCB change_completed_cb) { |
| // Find tracks -> Seek track -> run callback. |
| FindAndEnableProperTracks( |
| track_ids, curr_time, DemuxerStream::VIDEO, |
| track_ids.empty() ? std::move(change_completed_cb) |
| : base::BindOnce(&FFmpegDemuxer::SeekOnVideoTrackChange, |
| weak_factory_.GetWeakPtr(), curr_time, |
| std::move(change_completed_cb))); |
| } |
| |
| void FFmpegDemuxer::ReadFrameIfNeeded() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| // Make sure we have work to do before reading. |
| if (stopped_ || !StreamsHaveAvailableCapacity() || pending_read_ || |
| pending_seek_cb_) { |
| return; |
| } |
| |
| // Allocate and read an AVPacket from the media. Save |packet_ptr| since |
| // evaluation order of packet.get() and std::move(&packet) is |
| // undefined. |
| ScopedAVPacket packet = MakeScopedAVPacket(); |
| AVPacket* packet_ptr = packet.get(); |
| |
| pending_read_ = true; |
| base::PostTaskAndReplyWithResult( |
| blocking_task_runner_.get(), FROM_HERE, |
| base::BindOnce(&ReadFrameAndDiscardEmpty, glue_->format_context(), |
| packet_ptr), |
| base::BindOnce(&FFmpegDemuxer::OnReadFrameDone, |
| weak_factory_.GetWeakPtr(), std::move(packet))); |
| } |
| |
| void FFmpegDemuxer::OnReadFrameDone(ScopedAVPacket packet, int result) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(pending_read_); |
| pending_read_ = false; |
| |
| if (stopped_ || pending_seek_cb_) |
| 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()) { |
| if (result < 0) { |
| MEDIA_LOG(DEBUG, media_log_) |
| << GetDisplayName() |
| << ": av_read_frame(): " << AVErrorToString(result); |
| } else { |
| MEDIA_LOG(DEBUG, media_log_) |
| << GetDisplayName() << ": memory limit exceeded"; |
| } |
| |
| // Update the duration based on the highest elapsed time across all streams. |
| base::TimeDelta max_duration; |
| for (const auto& stream : streams_) { |
| if (!stream) |
| continue; |
| |
| base::TimeDelta duration = stream->duration(); |
| if (duration != kNoTimestamp && duration > max_duration) |
| max_duration = duration; |
| } |
| |
| if (duration_ == kInfiniteDuration || max_duration > duration_) { |
| host_->SetDuration(max_duration); |
| duration_known_ = true; |
| duration_ = max_duration; |
| } |
| |
| // If we have reached the end of stream, tell the downstream filters about |
| // the event. |
| StreamHasEnded(); |
| return; |
| } |
| |
| // Queue the packet with the appropriate stream; we must defend against ffmpeg |
| // giving us a bad stream index. See http://crbug.com/698549 for example. |
| if (packet->stream_index >= 0 && |
| static_cast<size_t>(packet->stream_index) < streams_.size()) { |
| // This is ensured by ReadFrameAndDiscardEmpty. |
| DCHECK(packet->data); |
| DCHECK(packet->size); |
| |
| if (auto& demuxer_stream = streams_[packet->stream_index]) { |
| if (demuxer_stream->IsEnabled()) |
| demuxer_stream->EnqueuePacket(std::move(packet)); |
| |
| // If duration estimate was incorrect, update it and tell higher layers. |
| if (duration_known_) { |
| const base::TimeDelta duration = demuxer_stream->duration(); |
| if (duration != kNoTimestamp && duration > duration_) { |
| duration_ = duration; |
| host_->SetDuration(duration_); |
| } |
| } |
| } |
| } |
| |
| // Keep reading until we've reached capacity. |
| ReadFrameIfNeeded(); |
| } |
| |
| bool FFmpegDemuxer::StreamsHaveAvailableCapacity() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| for (const auto& stream : streams_) { |
| if (stream && stream->IsEnabled() && stream->HasAvailableCapacity()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool FFmpegDemuxer::IsMaxMemoryUsageReached() const { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| size_t memory_left = |
| GetDemuxerMemoryLimit(Demuxer::DemuxerTypes::kFFmpegDemuxer); |
| 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_->RunsTasksInCurrentSequence()); |
| 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::NotifyDemuxerError(PipelineStatus status) { |
| MEDIA_LOG(ERROR, media_log_) << GetDisplayName() |
| << ": demuxer error: " << status; |
| host_->OnDemuxerError(status); |
| } |
| |
| void FFmpegDemuxer::SetLiveness(DemuxerStream::Liveness liveness) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| for (const auto& stream : streams_) { |
| if (stream) |
| stream->SetLiveness(liveness); |
| } |
| } |
| |
| void FFmpegDemuxer::RunInitCB(PipelineStatus status) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(init_cb_); |
| TRACE_EVENT_ASYNC_END1("media", "FFmpegDemuxer::Initialize", this, "status", |
| PipelineStatusToString(status)); |
| std::move(init_cb_).Run(status); |
| } |
| |
| void FFmpegDemuxer::RunPendingSeekCB(PipelineStatus status) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(pending_seek_cb_); |
| TRACE_EVENT_ASYNC_END1("media", "FFmpegDemuxer::Seek", this, "status", |
| PipelineStatusToString(status)); |
| std::move(pending_seek_cb_).Run(status); |
| } |
| |
| } // namespace media |