532 lines
16 KiB
C++
532 lines
16 KiB
C++
/******************************************************************************
|
|
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)
|