first commit
This commit is contained in:
531
project/fm_viewer/fav/AVMuxer.cpp
Normal file
531
project/fm_viewer/fav/AVMuxer.cpp
Normal file
@@ -0,0 +1,531 @@
|
||||
/******************************************************************************
|
||||
QtAV: Multimedia framework based on Qt and FFmpeg
|
||||
Copyright (C) 2012-2016 Wang Bin <wbsecg1@gmail.com>
|
||||
|
||||
* This file is part of QtAV (from 2015)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
******************************************************************************/
|
||||
|
||||
#include "AVMuxer.h"
|
||||
|
||||
#if !(REMOVE_AV_MUXER)
|
||||
|
||||
#include "AVCompat.h"
|
||||
#include "MediaIO.h"
|
||||
#include "VideoEncoder.h"
|
||||
#include "AudioEncoder.h"
|
||||
#include "internal.h"
|
||||
#include "Logger.h"
|
||||
|
||||
namespace FAV {
|
||||
static const char kFileScheme[] = "file:";
|
||||
#define CHAR_COUNT(s) (sizeof(s) - 1) // tail '\0'
|
||||
|
||||
// Packet::asAVPacket() assumes time base is 0.001
|
||||
static const AVRational kTB = {1, 1000};
|
||||
|
||||
class AVMuxer::Private
|
||||
{
|
||||
public:
|
||||
Private()
|
||||
: seekable(false)
|
||||
, network(false)
|
||||
, started(false)
|
||||
, eof(false)
|
||||
, media_changed(true)
|
||||
, format_ctx(0)
|
||||
, format(0)
|
||||
, io(0)
|
||||
, dict(0)
|
||||
, aenc(0)
|
||||
, venc(0)
|
||||
{
|
||||
av_register_all();
|
||||
}
|
||||
~Private() {
|
||||
//delete interrupt_hanlder;
|
||||
if (dict) {
|
||||
av_dict_free(&dict);
|
||||
dict = 0;
|
||||
}
|
||||
if (io) {
|
||||
delete io;
|
||||
io = 0;
|
||||
}
|
||||
}
|
||||
AVStream* addStream(AVFormatContext* ctx, const QString& codecName, AVCodecID codecId);
|
||||
bool prepareStreams();
|
||||
void applyOptionsForDict();
|
||||
void applyOptionsForContext();
|
||||
|
||||
bool seekable;
|
||||
bool network;
|
||||
bool started;
|
||||
bool eof;
|
||||
bool media_changed;
|
||||
AVFormatContext *format_ctx;
|
||||
//copy the info, not parse the file when constructed, then need member vars
|
||||
QString file;
|
||||
QString file_orig;
|
||||
AVOutputFormat *format;
|
||||
QString format_forced;
|
||||
MediaIO *io;
|
||||
|
||||
AVDictionary *dict;
|
||||
QVariantHash options;
|
||||
QList<int> audio_streams, video_streams, subtitle_streams;
|
||||
AudioEncoder *aenc; // not owner
|
||||
VideoEncoder *venc; // not owner
|
||||
};
|
||||
|
||||
AVStream *AVMuxer::Private::addStream(AVFormatContext* ctx, const QString &codecName, AVCodecID codecId)
|
||||
{
|
||||
AVCodec *codec = NULL;
|
||||
if (!codecName.isEmpty()) {
|
||||
codec = avcodec_find_encoder_by_name(codecName.toUtf8().constData());
|
||||
if (!codec) {
|
||||
const AVCodecDescriptor* cd = avcodec_descriptor_get_by_name(codecName.toUtf8().constData());
|
||||
if (cd) {
|
||||
codec = avcodec_find_encoder(cd->id);
|
||||
}
|
||||
}
|
||||
if (!codec)
|
||||
qWarning("Can not find encoder for %s", codecName.toUtf8().constData());
|
||||
} else if (codecId != QTAV_CODEC_ID(NONE)) {
|
||||
codec = avcodec_find_encoder(codecId);
|
||||
if (!codec)
|
||||
qWarning("Can not find encoder for %s", avcodec_get_name(codecId));
|
||||
}
|
||||
if (!codec)
|
||||
return 0;
|
||||
AVStream *s = avformat_new_stream(ctx, codec);
|
||||
if (!s) {
|
||||
qWarning("Can not allocate stream");
|
||||
return 0;
|
||||
}
|
||||
// set by avformat if unset
|
||||
s->id = ctx->nb_streams - 1;
|
||||
s->time_base = kTB;
|
||||
AVCodecContext *c = s->codec;
|
||||
c->codec_id = codec->id;
|
||||
// Using codec->time_base is deprecated, but needed for older lavf.
|
||||
c->time_base = s->time_base;
|
||||
/* Some formats want stream headers to be separate. */
|
||||
if (ctx->oformat->flags & AVFMT_GLOBALHEADER)
|
||||
c->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
||||
// expose avctx to encoder and set properties in encoder?
|
||||
// list codecs for a given format in ui
|
||||
return s;
|
||||
}
|
||||
|
||||
bool AVMuxer::Private::prepareStreams()
|
||||
{
|
||||
audio_streams.clear();
|
||||
video_streams.clear();
|
||||
subtitle_streams.clear();
|
||||
AVOutputFormat* fmt = format_ctx->oformat;
|
||||
if (venc) {
|
||||
AVStream *s = addStream(format_ctx, venc->codecName(), fmt->video_codec);
|
||||
if (s) {
|
||||
AVCodecContext *c = s->codec;
|
||||
c->bit_rate = venc->bitRate();
|
||||
c->width = venc->width();
|
||||
c->height = venc->height();
|
||||
/// MUST set after encoder is open to ensure format is valid and the same
|
||||
c->pix_fmt = (AVPixelFormat)VideoFormat::pixelFormatToFFmpeg(venc->pixelFormat());
|
||||
video_streams.push_back(s->id);
|
||||
}
|
||||
}
|
||||
if (aenc) {
|
||||
AVStream *s = addStream(format_ctx, aenc->codecName(), fmt->audio_codec);
|
||||
if (s) {
|
||||
AVCodecContext *c = s->codec;
|
||||
c->bit_rate = aenc->bitRate();
|
||||
/// MUST set after encoder is open to ensure format is valid and the same
|
||||
c->sample_rate = aenc->audioFormat().sampleRate();
|
||||
c->sample_fmt = (AVSampleFormat)aenc->audioFormat().sampleFormatFFmpeg();
|
||||
c->channel_layout = aenc->audioFormat().channelLayoutFFmpeg();
|
||||
c->channels = aenc->audioFormat().channels();
|
||||
c->bits_per_raw_sample = aenc->audioFormat().bytesPerSample()*8; // need??
|
||||
audio_streams.push_back(s->id);
|
||||
}
|
||||
}
|
||||
return !(audio_streams.isEmpty() && video_streams.isEmpty() && subtitle_streams.isEmpty());
|
||||
}
|
||||
|
||||
static void getFFmpegOutputFormats(QStringList* formats, QStringList* extensions)
|
||||
{
|
||||
static QStringList exts;
|
||||
static QStringList fmts;
|
||||
if (exts.isEmpty() && fmts.isEmpty()) {
|
||||
av_register_all(); // MUST register all input/output formats
|
||||
AVOutputFormat *o = NULL;
|
||||
QStringList e, f;
|
||||
while ((o = av_oformat_next(o))) {
|
||||
if (o->extensions)
|
||||
e << QString::fromLatin1(o->extensions).split(QLatin1Char(','), QString::SkipEmptyParts);
|
||||
if (o->name)
|
||||
f << QString::fromLatin1(o->name).split(QLatin1Char(','), QString::SkipEmptyParts);
|
||||
}
|
||||
foreach (const QString& v, e) {
|
||||
exts.append(v.trimmed());
|
||||
}
|
||||
foreach (const QString& v, f) {
|
||||
fmts.append(v.trimmed());
|
||||
}
|
||||
exts.removeDuplicates();
|
||||
fmts.removeDuplicates();
|
||||
}
|
||||
if (formats)
|
||||
*formats = fmts;
|
||||
if (extensions)
|
||||
*extensions = exts;
|
||||
}
|
||||
|
||||
const QStringList& AVMuxer::supportedFormats()
|
||||
{
|
||||
static QStringList fmts;
|
||||
if (fmts.isEmpty())
|
||||
getFFmpegOutputFormats(&fmts, NULL);
|
||||
return fmts;
|
||||
}
|
||||
|
||||
const QStringList& AVMuxer::supportedExtensions()
|
||||
{
|
||||
static QStringList exts;
|
||||
if (exts.isEmpty())
|
||||
getFFmpegOutputFormats(NULL, &exts);
|
||||
return exts;
|
||||
}
|
||||
|
||||
// TODO: move to FAV::supportedFormats(bool out). custom protols?
|
||||
const QStringList &AVMuxer::supportedProtocols()
|
||||
{
|
||||
static bool called = false;
|
||||
static QStringList protocols;
|
||||
if (called)
|
||||
return protocols;
|
||||
called = true;
|
||||
if (!protocols.isEmpty())
|
||||
return protocols;
|
||||
#if QTAV_HAVE(AVDEVICE)
|
||||
protocols << QStringLiteral("avdevice");
|
||||
#endif
|
||||
av_register_all(); // MUST register all input/output formats
|
||||
void* opq = 0;
|
||||
const char* protocol = avio_enum_protocols(&opq, 1);
|
||||
while (protocol) {
|
||||
// static string, no deep copy needed. but QByteArray::fromRawData(data,size) assumes data is not null terminated and we must give a size
|
||||
protocols.append(QString::fromUtf8(protocol));
|
||||
protocol = avio_enum_protocols(&opq, 1);
|
||||
}
|
||||
return protocols;
|
||||
}
|
||||
|
||||
AVMuxer::AVMuxer(QObject *parent)
|
||||
: QObject(parent)
|
||||
, d(new Private())
|
||||
{
|
||||
}
|
||||
|
||||
AVMuxer::~AVMuxer()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
QString AVMuxer::fileName() const
|
||||
{
|
||||
return d->file_orig;
|
||||
}
|
||||
|
||||
QIODevice* AVMuxer::ioDevice() const
|
||||
{
|
||||
if (!d->io)
|
||||
return 0;
|
||||
if (d->io->name() != QLatin1String("QIODevice"))
|
||||
return 0;
|
||||
return d->io->property("device").value<QIODevice*>();
|
||||
}
|
||||
|
||||
MediaIO* AVMuxer::mediaIO() const
|
||||
{
|
||||
return d->io;
|
||||
}
|
||||
|
||||
bool AVMuxer::setMedia(const QString &fileName)
|
||||
{
|
||||
if (d->io) {
|
||||
delete d->io;
|
||||
d->io = 0;
|
||||
}
|
||||
d->file_orig = fileName;
|
||||
const QString url_old(d->file);
|
||||
d->file = fileName.trimmed();
|
||||
if (d->file.startsWith(QLatin1String("mms:")))
|
||||
d->file.insert(3, QLatin1Char('h'));
|
||||
else if (d->file.startsWith(QLatin1String(kFileScheme)))
|
||||
d->file = Internal::Path::toLocal(d->file);
|
||||
int colon = d->file.indexOf(QLatin1Char(':'));
|
||||
if (colon == 1) {
|
||||
#ifdef Q_OS_WINRT
|
||||
d->file.prepend(QStringLiteral("qfile:"));
|
||||
#endif
|
||||
}
|
||||
d->media_changed = url_old != d->file;
|
||||
if (d->media_changed) {
|
||||
d->format_forced.clear();
|
||||
}
|
||||
// a local file. return here to avoid protocol checking. If path contains ":", protocol checking will fail
|
||||
if (d->file.startsWith(QLatin1Char('/')))
|
||||
return d->media_changed;
|
||||
// use MediaIO to support protocols not supported by ffmpeg
|
||||
colon = d->file.indexOf(QLatin1Char(':'));
|
||||
if (colon >= 0) {
|
||||
#ifdef Q_OS_WIN
|
||||
if (colon == 1 && d->file.at(0).isLetter())
|
||||
return d->media_changed;
|
||||
#endif
|
||||
const QString scheme = colon == 0 ? QStringLiteral("qrc") : d->file.left(colon);
|
||||
// supportedProtocols() is not complete. so try MediaIO 1st, if not found, fallback to libavformat
|
||||
d->io = MediaIO::createForProtocol(scheme);
|
||||
if (d->io) {
|
||||
d->io->setUrl(d->file);
|
||||
}
|
||||
}
|
||||
return d->media_changed;
|
||||
}
|
||||
|
||||
bool AVMuxer::setMedia(QIODevice* device)
|
||||
{
|
||||
d->file = QString();
|
||||
d->file_orig = QString();
|
||||
if (d->io) {
|
||||
if (d->io->name() != QLatin1String("QIODevice")) {
|
||||
delete d->io;
|
||||
d->io = 0;
|
||||
}
|
||||
}
|
||||
if (!d->io)
|
||||
d->io = MediaIO::create("QIODevice");
|
||||
QIODevice* old_dev = d->io->property("device").value<QIODevice*>();
|
||||
d->media_changed = old_dev != device;
|
||||
if (d->media_changed) {
|
||||
d->format_forced.clear();
|
||||
}
|
||||
d->io->setProperty("device", QVariant::fromValue(device)); //open outside?
|
||||
return d->media_changed;
|
||||
}
|
||||
|
||||
bool AVMuxer::setMedia(MediaIO *in)
|
||||
{
|
||||
d->media_changed = in != d->io;
|
||||
if (d->media_changed) {
|
||||
d->format_forced.clear();
|
||||
}
|
||||
d->file = QString();
|
||||
d->file_orig = QString();
|
||||
if (!d->io)
|
||||
d->io = in;
|
||||
if (d->io != in) {
|
||||
delete d->io;
|
||||
d->io = in;
|
||||
}
|
||||
return d->media_changed;
|
||||
}
|
||||
|
||||
void AVMuxer::setFormat(const QString &fmt)
|
||||
{
|
||||
d->format_forced = fmt;
|
||||
}
|
||||
|
||||
QString AVMuxer::formatForced() const
|
||||
{
|
||||
return d->format_forced;
|
||||
}
|
||||
|
||||
bool AVMuxer::open()
|
||||
{
|
||||
// avformatcontext will be allocated in avformat_alloc_output_context2()
|
||||
//d->format_ctx->interrupt_callback = *d->interrupt_hanlder;
|
||||
|
||||
d->applyOptionsForDict();
|
||||
// check special dict keys
|
||||
// d->format_forced can be set from AVFormatContext.format_whitelist
|
||||
if (!d->format_forced.isEmpty()) {
|
||||
d->format = av_guess_format(d->format_forced.toUtf8().constData(), NULL, NULL);
|
||||
qDebug() << "force format: " << d->format_forced;
|
||||
}
|
||||
|
||||
//d->interrupt_hanlder->begin(InterruptHandler::Open);
|
||||
if (d->io) {
|
||||
if (d->io->accessMode() == MediaIO::Read) {
|
||||
qWarning("wrong MediaIO accessMode. MUST be Write");
|
||||
}
|
||||
AV_ENSURE_OK(avformat_alloc_output_context2(&d->format_ctx, d->format, d->format_forced.isEmpty() ? 0 : d->format_forced.toUtf8().constData(), ""), false);
|
||||
d->format_ctx->pb = (AVIOContext*)d->io->avioContext();
|
||||
d->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
//d->format_ctx->flags |= AVFMT_FLAG_GENPTS;
|
||||
} else {
|
||||
AV_ENSURE_OK(avformat_alloc_output_context2(&d->format_ctx, d->format, d->format_forced.isEmpty() ? 0 : d->format_forced.toUtf8().constData(), fileName().toUtf8().constData()), false);
|
||||
}
|
||||
//d->interrupt_hanlder->end();
|
||||
|
||||
if (!d->prepareStreams()) {
|
||||
return false;
|
||||
}
|
||||
// TODO: AVFMT_NOFILE ? examples/muxing.c only check AVFMT_NOFILE
|
||||
// a custome io does not need avio_open. it open resource in it's own way, e.g. QIODevice.open
|
||||
if (!(d->format_ctx->oformat->flags & AVFMT_NOFILE) && !(d->format_ctx->flags & AVFMT_FLAG_CUSTOM_IO)) {
|
||||
// avio_open2?
|
||||
AV_ENSURE_OK(avio_open(&d->format_ctx->pb, fileName().toUtf8().constData(), AVIO_FLAG_WRITE), false);
|
||||
}
|
||||
// d->format_ctx->start_time_realtime
|
||||
AV_ENSURE_OK(avformat_write_header(d->format_ctx, &d->dict), false);
|
||||
d->started = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AVMuxer::close()
|
||||
{
|
||||
if (!isOpen())
|
||||
return true;
|
||||
av_write_trailer(d->format_ctx);
|
||||
// close AVCodecContext* in encoder
|
||||
// custom io will call avio_close in ~MediaIO()
|
||||
if (!(d->format_ctx->oformat->flags & AVFMT_NOFILE) && !(d->format_ctx->flags & AVFMT_FLAG_CUSTOM_IO)) {
|
||||
if (d->format_ctx->pb) {
|
||||
avio_flush(d->format_ctx->pb);
|
||||
avio_close(d->format_ctx->pb);
|
||||
d->format_ctx->pb = 0;
|
||||
}
|
||||
}
|
||||
avformat_free_context(d->format_ctx);
|
||||
d->format_ctx = 0;
|
||||
d->audio_streams.clear();
|
||||
d->video_streams.clear();
|
||||
d->subtitle_streams.clear();
|
||||
d->started = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AVMuxer::isOpen() const
|
||||
{
|
||||
return d->format_ctx;
|
||||
}
|
||||
|
||||
bool AVMuxer::writeAudio(const FAV::Packet& packet)
|
||||
{
|
||||
AVPacket *pkt = (AVPacket*)packet.asAVPacket(); //FIXME
|
||||
pkt->stream_index = d->audio_streams[0]; //FIXME
|
||||
AVStream *s = d->format_ctx->streams[pkt->stream_index];
|
||||
// stream.time_base is set in avformat_write_header
|
||||
av_packet_rescale_ts(pkt, kTB, s->time_base);
|
||||
av_interleaved_write_frame(d->format_ctx, pkt);
|
||||
|
||||
d->started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AVMuxer::writeVideo(const FAV::Packet& packet)
|
||||
{
|
||||
AVPacket *pkt = (AVPacket*)packet.asAVPacket();
|
||||
pkt->stream_index = d->video_streams[0];
|
||||
AVStream *s = d->format_ctx->streams[pkt->stream_index];
|
||||
// stream.time_base is set in avformat_write_header
|
||||
av_packet_rescale_ts(pkt, kTB, s->time_base);
|
||||
//av_write_frame
|
||||
av_interleaved_write_frame(d->format_ctx, pkt);
|
||||
#if 0
|
||||
qDebug("mux packet.pts: %.3f dts:%.3f duration: %.3f, avpkt.pts: %lld,dts:%lld,duration:%lld"
|
||||
, packet.pts, packet.dts, packet.duration
|
||||
, pkt->pts, pkt->dts, pkt->duration);
|
||||
qDebug("stream: %d duration: %lld, end: %lld. tb:{%d/%d}"
|
||||
, pkt->stream_index, s->duration
|
||||
, av_stream_get_end_pts(s)
|
||||
, s->time_base.num, s->time_base.den
|
||||
);
|
||||
#endif
|
||||
d->started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AVMuxer::copyProperties(VideoEncoder *enc)
|
||||
{
|
||||
d->venc = enc;
|
||||
}
|
||||
|
||||
void AVMuxer::copyProperties(AudioEncoder *enc)
|
||||
{
|
||||
d->aenc = enc;
|
||||
}
|
||||
|
||||
void AVMuxer::setOptions(const QVariantHash &dict)
|
||||
{
|
||||
d->options = dict;
|
||||
d->applyOptionsForContext(); // apply even if avformat context is open
|
||||
}
|
||||
|
||||
QVariantHash AVMuxer::options() const
|
||||
{
|
||||
return d->options;
|
||||
}
|
||||
|
||||
void AVMuxer::Private::applyOptionsForDict()
|
||||
{
|
||||
if (dict) {
|
||||
av_dict_free(&dict);
|
||||
dict = 0; //aready 0 in av_free
|
||||
}
|
||||
if (options.isEmpty())
|
||||
return;
|
||||
QVariant opt(options);
|
||||
if (options.contains(QStringLiteral("avformat")))
|
||||
opt = options.value(QStringLiteral("avformat"));
|
||||
Internal::setOptionsToDict(opt, &dict);
|
||||
|
||||
if (opt.type() == QVariant::Map) {
|
||||
QVariantMap avformat_dict(opt.toMap());
|
||||
if (avformat_dict.contains(QStringLiteral("format_whitelist"))) {
|
||||
const QString fmts(avformat_dict[QStringLiteral("format_whitelist")].toString());
|
||||
if (!fmts.contains(QLatin1Char(',')) && !fmts.isEmpty())
|
||||
format_forced = fmts; // reset when media changed
|
||||
}
|
||||
} else if (opt.type() == QVariant::Hash) {
|
||||
QVariantHash avformat_dict(opt.toHash());
|
||||
if (avformat_dict.contains(QStringLiteral("format_whitelist"))) {
|
||||
const QString fmts(avformat_dict[QStringLiteral("format_whitelist")].toString());
|
||||
if (!fmts.contains(QLatin1Char(',')) && !fmts.isEmpty())
|
||||
format_forced = fmts; // reset when media changed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AVMuxer::Private::applyOptionsForContext()
|
||||
{
|
||||
if (!format_ctx)
|
||||
return;
|
||||
if (options.isEmpty()) {
|
||||
//av_opt_set_defaults(format_ctx); //can't set default values! result maybe unexpected
|
||||
return;
|
||||
}
|
||||
QVariant opt(options);
|
||||
if (options.contains(QStringLiteral("avformat")))
|
||||
opt = options.value(QStringLiteral("avformat"));
|
||||
Internal::setOptionsToFFmpegObj(opt, format_ctx);
|
||||
}
|
||||
} //namespace FAV
|
||||
#endif // #if !(REMOVE_AV_MUXER)
|
||||
Reference in New Issue
Block a user