You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
332 lines
9.3 KiB
332 lines
9.3 KiB
#include <QDebug>
|
|
#include <QFileInfo>
|
|
#include <QCoreApplication>
|
|
|
|
#include "compat.h"
|
|
#include "recorder.h"
|
|
|
|
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
|
|
|
Recorder::Recorder(const QString& fileName, QObject* parent)
|
|
: QThread(parent)
|
|
, m_fileName(fileName)
|
|
, m_format(guessRecordFormat(fileName))
|
|
{
|
|
}
|
|
|
|
Recorder::~Recorder()
|
|
{
|
|
}
|
|
|
|
AVPacket* Recorder::packetNew(const AVPacket *packet) {
|
|
AVPacket* rec = new AVPacket;
|
|
if (!rec) {
|
|
return Q_NULLPTR;
|
|
}
|
|
|
|
// av_packet_ref() does not initialize all fields in old FFmpeg versions
|
|
av_init_packet(rec);
|
|
|
|
if (av_packet_ref(rec, packet)) {
|
|
delete rec;
|
|
return Q_NULLPTR;
|
|
}
|
|
return rec;
|
|
}
|
|
|
|
void Recorder::packetDelete(AVPacket* packet) {
|
|
av_packet_unref(packet);
|
|
delete packet;
|
|
}
|
|
|
|
void Recorder::queueClear()
|
|
{
|
|
while (!m_queue.isEmpty()) {
|
|
packetDelete(m_queue.dequeue());
|
|
}
|
|
}
|
|
|
|
void Recorder::setFrameSize(const QSize &declaredFrameSize)
|
|
{
|
|
m_declaredFrameSize = declaredFrameSize;
|
|
}
|
|
|
|
void Recorder::setFormat(Recorder::RecorderFormat format)
|
|
{
|
|
m_format = format;
|
|
}
|
|
|
|
bool Recorder::open(const AVCodec* inputCodec)
|
|
{
|
|
QString formatName = recorderGetFormatName(m_format);
|
|
Q_ASSERT(!formatName.isEmpty());
|
|
const AVOutputFormat* format = findMuxer(formatName.toUtf8());
|
|
if (!format) {
|
|
qCritical("Could not find muxer");
|
|
return false;
|
|
}
|
|
|
|
m_formatCtx = avformat_alloc_context();
|
|
if (!m_formatCtx) {
|
|
qCritical("Could not allocate output context");
|
|
return false;
|
|
}
|
|
|
|
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
|
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
|
|
// still expects a pointer-to-non-const (it has not be updated accordingly)
|
|
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
|
|
|
|
m_formatCtx->oformat = (AVOutputFormat*)format;
|
|
|
|
QString comment = "Recorded by QtScrcpy " + QCoreApplication::applicationVersion();
|
|
av_dict_set(&m_formatCtx->metadata, "comment",
|
|
comment.toUtf8(), 0);
|
|
|
|
AVStream* outStream = avformat_new_stream(m_formatCtx, inputCodec);
|
|
if (!outStream) {
|
|
avformat_free_context(m_formatCtx);
|
|
m_formatCtx = Q_NULLPTR;
|
|
return false;
|
|
}
|
|
|
|
#ifdef QTSCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
|
|
outStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
|
outStream->codecpar->codec_id = inputCodec->id;
|
|
outStream->codecpar->format = AV_PIX_FMT_YUV420P;
|
|
outStream->codecpar->width = m_declaredFrameSize.width();
|
|
outStream->codecpar->height = m_declaredFrameSize.height();
|
|
#else
|
|
outStream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
|
|
outStream->codec->codec_id = inputCodec->id;
|
|
outStream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
outStream->codec->width = m_declaredFrameSize.width();
|
|
outStream->codec->height = m_declaredFrameSize.height();
|
|
#endif
|
|
|
|
int ret = avio_open(&m_formatCtx->pb, m_fileName.toUtf8().toStdString().c_str(),
|
|
AVIO_FLAG_WRITE);
|
|
if (ret < 0) {
|
|
char errorbuf[255] = { 0 };
|
|
av_strerror(ret, errorbuf, 254);
|
|
qCritical(QString("Failed to open output file: %1 %2").arg(errorbuf).arg(m_fileName).toUtf8().toStdString().c_str());
|
|
// ostream will be cleaned up during context cleaning
|
|
avformat_free_context(m_formatCtx);
|
|
m_formatCtx = Q_NULLPTR;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Recorder::close()
|
|
{
|
|
if (Q_NULLPTR != m_formatCtx) {
|
|
if (m_headerWritten) {
|
|
int ret = av_write_trailer(m_formatCtx);
|
|
if (ret < 0) {
|
|
qCritical(QString("Failed to write trailer to %1").arg(m_fileName).toUtf8().toStdString().c_str());
|
|
m_failed = true;
|
|
} else {
|
|
qInfo(QString("success record %1").arg(m_fileName).toStdString().c_str());
|
|
}
|
|
} else {
|
|
// the recorded file is empty
|
|
m_failed = true;
|
|
}
|
|
avio_close(m_formatCtx->pb);
|
|
avformat_free_context(m_formatCtx);
|
|
m_formatCtx = Q_NULLPTR;
|
|
}
|
|
}
|
|
|
|
bool Recorder::write(AVPacket *packet)
|
|
{
|
|
if (!m_headerWritten) {
|
|
if (packet->pts != AV_NOPTS_VALUE) {
|
|
qCritical("The first packet is not a config packet");
|
|
return false;
|
|
}
|
|
bool ok = recorderWriteHeader(packet);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
m_headerWritten = true;
|
|
return true;
|
|
}
|
|
|
|
if (packet->pts == AV_NOPTS_VALUE) {
|
|
// ignore config packets
|
|
return true;
|
|
}
|
|
|
|
recorderRescalePacket(packet);
|
|
return av_write_frame(m_formatCtx, packet) >= 0;
|
|
}
|
|
|
|
const AVOutputFormat *Recorder::findMuxer(const char* name)
|
|
{
|
|
#ifdef QTSCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
|
|
void* opaque = Q_NULLPTR;
|
|
#endif
|
|
const AVOutputFormat* outFormat = Q_NULLPTR;
|
|
do {
|
|
#ifdef QTSCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
|
|
outFormat = av_muxer_iterate(&opaque);
|
|
#else
|
|
outFormat = av_oformat_next(outFormat);
|
|
#endif
|
|
// until null or with name "name"
|
|
} while (outFormat && strcmp(outFormat->name, name));
|
|
return outFormat;
|
|
}
|
|
|
|
bool Recorder::recorderWriteHeader(const AVPacket* packet)
|
|
{
|
|
AVStream *ostream = m_formatCtx->streams[0];
|
|
quint8* extradata = (quint8*)av_malloc(packet->size * sizeof(quint8));
|
|
if (!extradata) {
|
|
qCritical("Cannot allocate extradata");
|
|
return false;
|
|
}
|
|
// copy the first packet to the extra data
|
|
memcpy(extradata, packet->data, packet->size);
|
|
|
|
#ifdef QTSCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
|
|
ostream->codecpar->extradata = extradata;
|
|
ostream->codecpar->extradata_size = packet->size;
|
|
#else
|
|
ostream->codec->extradata = extradata;
|
|
ostream->codec->extradata_size = packet->size;
|
|
#endif
|
|
|
|
int ret = avformat_write_header(m_formatCtx, NULL);
|
|
if (ret < 0) {
|
|
qCritical("Failed to write header recorder file");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Recorder::recorderRescalePacket(AVPacket* packet)
|
|
{
|
|
AVStream *ostream = m_formatCtx->streams[0];
|
|
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
|
}
|
|
|
|
QString Recorder::recorderGetFormatName(Recorder::RecorderFormat format)
|
|
{
|
|
switch (format) {
|
|
case RECORDER_FORMAT_MP4: return "mp4";
|
|
case RECORDER_FORMAT_MKV: return "matroska";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
Recorder::RecorderFormat Recorder::guessRecordFormat(const QString &fileName)
|
|
{
|
|
if (4 > fileName.length()) {
|
|
return Recorder::RECORDER_FORMAT_NULL;
|
|
}
|
|
QFileInfo fileInfo = QFileInfo(fileName);
|
|
QString ext = fileInfo.suffix();
|
|
if (0 == ext.compare("mp4")) {
|
|
return Recorder::RECORDER_FORMAT_MP4;
|
|
}
|
|
if (0 == ext.compare("mkv")) {
|
|
return Recorder::RECORDER_FORMAT_MKV;
|
|
}
|
|
|
|
return Recorder::RECORDER_FORMAT_NULL;
|
|
}
|
|
|
|
void Recorder::run() {
|
|
for (;;) {
|
|
AVPacket *rec = Q_NULLPTR;
|
|
{
|
|
QMutexLocker locker(&m_mutex);
|
|
while (!m_stopped && m_queue.isEmpty()) {
|
|
m_recvDataCond.wait(&m_mutex);
|
|
}
|
|
|
|
// if stopped is set, continue to process the remaining events (to
|
|
// finish the recording) before actually stopping
|
|
if (m_stopped && m_queue.isEmpty()) {
|
|
AVPacket* last = m_previous;
|
|
if (last) {
|
|
// assign an arbitrary duration to the last packet
|
|
last->duration = 100000;
|
|
bool ok = write(last);
|
|
if (!ok) {
|
|
// failing to write the last frame is not very serious, no
|
|
// future frame may depend on it, so the resulting file
|
|
// will still be valid
|
|
qWarning("Could not record last packet");
|
|
}
|
|
packetDelete(last);
|
|
}
|
|
break;
|
|
}
|
|
|
|
rec = m_queue.dequeue();
|
|
}
|
|
|
|
// recorder->previous is only written from this thread, no need to lock
|
|
AVPacket* previous = m_previous;
|
|
m_previous = rec;
|
|
|
|
if (!previous) {
|
|
// we just received the first packet
|
|
continue;
|
|
}
|
|
|
|
// config packets have no PTS, we must ignore them
|
|
if (rec->pts != AV_NOPTS_VALUE
|
|
&& previous->pts != AV_NOPTS_VALUE) {
|
|
// we now know the duration of the previous packet
|
|
previous->duration = rec->pts - previous->pts;
|
|
}
|
|
|
|
bool ok = write(previous);
|
|
packetDelete(previous);
|
|
if (!ok) {
|
|
qCritical("Could not record packet");
|
|
QMutexLocker locker(&m_mutex);
|
|
m_failed = true;
|
|
// discard pending packets
|
|
queueClear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
qDebug("Recorder thread ended");
|
|
}
|
|
|
|
bool Recorder::startRecorder() {
|
|
start();
|
|
return true;
|
|
}
|
|
|
|
void Recorder::stopRecorder() {
|
|
QMutexLocker locker(&m_mutex);
|
|
m_stopped = true;
|
|
m_recvDataCond.wakeOne();
|
|
}
|
|
|
|
bool Recorder::push(const AVPacket *packet) {
|
|
QMutexLocker locker(&m_mutex);
|
|
assert(!m_stopped);
|
|
|
|
if (m_failed) {
|
|
// reject any new packet (this will stop the stream)
|
|
return false;
|
|
}
|
|
|
|
AVPacket* rec = packetNew(packet);
|
|
if (rec) {
|
|
m_queue.enqueue(rec);
|
|
m_recvDataCond.wakeOne();
|
|
}
|
|
return rec != Q_NULLPTR;
|
|
}
|