1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
media / filters / ffmpeg_glue.cc [blame]
// 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/base/supported_types.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");
#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;
// We don't allow H.264 parsing during demuxing since we have our own parser
// and the ffmpeg one increases memory usage unnecessarily.
format_context_->flags |= AVFMT_FLAG_NOH264PARSE;
// 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();
// 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.
//
// We also don't allow ffmpeg to use any video decoders during demuxing
// since it's unnecessary for the codecs we use and just increases
// memory usage.
//
// Note: FFmpeg will try to free these strings, so we must duplicate them.
format_context_->codec_whitelist = av_strdup(GetAllowedAudioDecoders());
format_context_->format_whitelist = av_strdup(GetAllowedDemuxers());
}
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;
// We need to set the WAV decoder max size to what it had previously been set
// to. The auto-selectable max size ends up at 64k, which is larger than the
// read size from a MultiBufferDataSource, causing demuxer init to never
// complete.
AVDictionary* options = nullptr;
av_dict_set(&options, "max_size", "4096", 0);
// 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_.AsEphemeralRawAddr(),
nullptr, nullptr, &options);
if (options) {
av_dict_free(&options);
}
// 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 successfully 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_.AsEphemeralRawAddr());
av_free(avio_context_->buffer);
}
} // namespace media