| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/filters/ffmpeg_glue.h" |
| |
| #include "base/check_op.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/no_destructor.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_util.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "media/base/container_names.h" |
| #include "media/base/media_switches.h" |
| #include "media/ffmpeg/ffmpeg_common.h" |
| |
| namespace media { |
| |
| // Internal buffer size used by AVIO for reading. |
| // TODO(dalecurtis): Experiment with this buffer size and measure impact on |
| // performance. Currently we want to use 32kb to preserve existing behavior |
| // with the previous URLProtocol based approach. |
| enum { kBufferSize = 32 * 1024 }; |
| |
| static int AVIOReadOperation(void* opaque, uint8_t* buf, int buf_size) { |
| return reinterpret_cast<FFmpegURLProtocol*>(opaque)->Read(buf_size, buf); |
| } |
| |
| static int64_t AVIOSeekOperation(void* opaque, int64_t offset, int whence) { |
| FFmpegURLProtocol* protocol = reinterpret_cast<FFmpegURLProtocol*>(opaque); |
| int64_t new_offset = AVERROR(EIO); |
| switch (whence) { |
| case SEEK_SET: |
| if (protocol->SetPosition(offset)) |
| protocol->GetPosition(&new_offset); |
| break; |
| |
| case SEEK_CUR: |
| int64_t pos; |
| if (!protocol->GetPosition(&pos)) |
| break; |
| if (protocol->SetPosition(pos + offset)) |
| protocol->GetPosition(&new_offset); |
| break; |
| |
| case SEEK_END: |
| int64_t size; |
| if (!protocol->GetSize(&size)) |
| break; |
| if (protocol->SetPosition(size + offset)) |
| protocol->GetPosition(&new_offset); |
| break; |
| |
| case AVSEEK_SIZE: |
| protocol->GetSize(&new_offset); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| return new_offset; |
| } |
| |
| static void LogContainer(bool is_local_file, |
| container_names::MediaContainerName container) { |
| base::UmaHistogramSparse("Media.DetectedContainer", |
| base::to_underlying(container)); |
| if (is_local_file) { |
| base::UmaHistogramSparse("Media.DetectedContainer.Local", |
| base::to_underlying(container)); |
| } |
| } |
| |
| static const char* GetAllowedDemuxers() { |
| static const base::NoDestructor<std::string> kAllowedDemuxers([]() { |
| // This should match the configured lists in //third_party/ffmpeg. |
| std::vector<std::string> allowed_demuxers = {"ogg", "matroska", "wav", |
| "flac", "mp3", "mov"}; |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| allowed_demuxers.push_back("aac"); |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (base::FeatureList::IsEnabled(kCrOSLegacyMediaFormats)) { |
| allowed_demuxers.push_back("avi"); |
| } |
| #endif |
| #endif |
| return base::JoinString(allowed_demuxers, ","); |
| }()); |
| return kAllowedDemuxers->c_str(); |
| } |
| |
| FFmpegGlue::FFmpegGlue(FFmpegURLProtocol* protocol) { |
| // Initialize an AVIOContext using our custom read and seek operations. Don't |
| // keep pointers to the buffer since FFmpeg may reallocate it on the fly. It |
| // will be cleaned up |
| format_context_ = avformat_alloc_context(); |
| avio_context_.reset(avio_alloc_context( |
| static_cast<unsigned char*>(av_malloc(kBufferSize)), kBufferSize, 0, |
| protocol, &AVIOReadOperation, nullptr, &AVIOSeekOperation)); |
| |
| // Ensure FFmpeg only tries to seek on resources we know to be seekable. |
| avio_context_->seekable = |
| protocol->IsStreaming() ? 0 : AVIO_SEEKABLE_NORMAL; |
| |
| // Ensure writing is disabled. |
| avio_context_->write_flag = 0; |
| |
| // Tell the format context about our custom IO context. avformat_open_input() |
| // will set the AVFMT_FLAG_CUSTOM_IO flag for us, but do so here to ensure an |
| // early error state doesn't cause FFmpeg to free our resources in error. |
| format_context_->flags |= AVFMT_FLAG_CUSTOM_IO; |
| |
| // Enable fast, but inaccurate seeks for MP3. |
| format_context_->flags |= AVFMT_FLAG_FAST_SEEK; |
| |
| // Ensures format parsing errors will bail out. From an audit on 11/2017, all |
| // instances were real failures. Solves bugs like http://crbug.com/710791. |
| format_context_->error_recognition |= AV_EF_EXPLODE; |
| |
| format_context_->pb = avio_context_.get(); |
| |
| if (base::FeatureList::IsEnabled(kFFmpegAllowLists)) { |
| // Enhance security by forbidding ffmpeg from decoding / demuxing codecs and |
| // containers which should be unsupported. |
| // |
| // Normally these aren't even compiled in, but during codec/container |
| // deprecations and when an external ffmpeg is used this adds extra |
| // security. |
| static const base::NoDestructor<std::string> kCombinedCodecList([]() { |
| return base::JoinString( |
| {GetAllowedAudioDecoders(), GetAllowedVideoDecoders()}, ","); |
| }()); |
| |
| // Note: FFmpeg will try to free these strings, so we must duplicate them. |
| format_context_->codec_whitelist = av_strdup(kCombinedCodecList->c_str()); |
| format_context_->format_whitelist = av_strdup(GetAllowedDemuxers()); |
| } |
| } |
| |
| // static |
| const char* FFmpegGlue::GetAllowedAudioDecoders() { |
| static const base::NoDestructor<std::string> kAllowedAudioCodecs([]() { |
| // This should match the configured lists in //third_party/ffmpeg. |
| std::string allowed_decoders( |
| "vorbis,libopus,flac,pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le," |
| "mp3,pcm_s16be,pcm_s24be,pcm_mulaw,pcm_alaw"); |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| allowed_decoders += ",aac"; |
| #endif |
| return allowed_decoders; |
| }()); |
| return kAllowedAudioCodecs->c_str(); |
| } |
| |
| // static |
| const char* FFmpegGlue::GetAllowedVideoDecoders() { |
| static const base::NoDestructor<std::string> kAllowedVideoCodecs([]() { |
| // This should match the configured lists in //third_party/ffmpeg. |
| #if BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS) |
| std::vector<std::string> allowed_decoders; |
| if (base::FeatureList::IsEnabled(kTheoraVideoCodec)) { |
| allowed_decoders.push_back("theora"); |
| } |
| if (base::FeatureList::IsEnabled(kFFmpegDecodeOpaqueVP8)) { |
| allowed_decoders.push_back("vp8"); |
| } |
| #if BUILDFLAG(USE_PROPRIETARY_CODECS) |
| allowed_decoders.push_back("h264"); |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (base::FeatureList::IsEnabled(kCrOSLegacyMediaFormats)) { |
| allowed_decoders.push_back("mpeg4"); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| #endif // BUILDFLAG(USE_PROPRIETARY_CODECS) |
| return base::JoinString(allowed_decoders, ","); |
| #else |
| return std::string(); |
| #endif // BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS) |
| }()); |
| return kAllowedVideoCodecs->c_str(); |
| } |
| |
| bool FFmpegGlue::OpenContext(bool is_local_file) { |
| DCHECK(!open_called_) << "OpenContext() shouldn't be called twice."; |
| |
| // If avformat_open_input() is called we have to take a slightly different |
| // destruction path to avoid double frees. |
| open_called_ = true; |
| |
| // By passing nullptr for the filename (second parameter) we are telling |
| // FFmpeg to use the AVIO context we setup from the AVFormatContext structure. |
| const int ret = |
| avformat_open_input(&format_context_, nullptr, nullptr, nullptr); |
| |
| // If FFmpeg can't identify the file, read the first 8k and attempt to guess |
| // at the container type ourselves. This way we can track emergent formats. |
| // Only try on AVERROR_INVALIDDATA to avoid running after I/O errors. |
| if (ret == AVERROR_INVALIDDATA) { |
| std::vector<uint8_t> buffer(8192); |
| |
| const int64_t pos = AVIOSeekOperation(avio_context_->opaque, 0, SEEK_SET); |
| if (pos < 0) |
| return false; |
| |
| const int num_read = |
| AVIOReadOperation(avio_context_->opaque, buffer.data(), buffer.size()); |
| if (num_read < container_names::kMinimumContainerSize) |
| return false; |
| |
| container_ = container_names::DetermineContainer(buffer.data(), num_read); |
| LogContainer(is_local_file, container_); |
| |
| detected_hls_ = |
| container_ == container_names::MediaContainerName::kContainerHLS; |
| return false; |
| } else if (ret < 0) { |
| return false; |
| } |
| |
| // Rely on ffmpeg's parsing if we're able to succesfully open the file. |
| if (strcmp(format_context_->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0) |
| container_ = container_names::MediaContainerName::kContainerMOV; |
| else if (strcmp(format_context_->iformat->name, "flac") == 0) |
| container_ = container_names::MediaContainerName::kContainerFLAC; |
| else if (strcmp(format_context_->iformat->name, "matroska,webm") == 0) |
| container_ = container_names::MediaContainerName::kContainerWEBM; |
| else if (strcmp(format_context_->iformat->name, "ogg") == 0) |
| container_ = container_names::MediaContainerName::kContainerOgg; |
| else if (strcmp(format_context_->iformat->name, "wav") == 0) |
| container_ = container_names::MediaContainerName::kContainerWAV; |
| else if (strcmp(format_context_->iformat->name, "aac") == 0) |
| container_ = container_names::MediaContainerName::kContainerAAC; |
| else if (strcmp(format_context_->iformat->name, "mp3") == 0) |
| container_ = container_names::MediaContainerName::kContainerMP3; |
| else if (strcmp(format_context_->iformat->name, "amr") == 0) |
| container_ = container_names::MediaContainerName::kContainerAMR; |
| else if (strcmp(format_context_->iformat->name, "avi") == 0) |
| container_ = container_names::MediaContainerName::kContainerAVI; |
| |
| // For a successfully opened file, we will get a container we've compiled in. |
| CHECK_NE(container_, container_names::MediaContainerName::kContainerUnknown); |
| LogContainer(is_local_file, container_); |
| |
| return true; |
| } |
| |
| FFmpegGlue::~FFmpegGlue() { |
| // In the event of avformat_open_input() failure, FFmpeg may sometimes free |
| // our AVFormatContext behind the scenes, but leave the buffer alive. It will |
| // helpfully set |format_context_| to nullptr in this case. |
| if (!format_context_) { |
| av_free(avio_context_->buffer); |
| return; |
| } |
| |
| // If avformat_open_input() hasn't been called, we should simply free the |
| // AVFormatContext and buffer instead of using avformat_close_input(). |
| if (!open_called_) { |
| avformat_free_context(format_context_); |
| av_free(avio_context_->buffer); |
| return; |
| } |
| |
| avformat_close_input(&format_context_); |
| av_free(avio_context_->buffer); |
| } |
| |
| } // namespace media |