init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/clip/media_clip_check_streaming.h"
|
||||
|
||||
#include "core/file_location.h"
|
||||
#include "base/bytes.h"
|
||||
#include "logs.h"
|
||||
|
||||
#include <QtCore/QtEndian>
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtCore/QFile>
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
namespace {
|
||||
|
||||
constexpr auto kHeaderSize = 8;
|
||||
constexpr auto kFindMoovBefore = 128 * 1024;
|
||||
|
||||
template <typename Type>
|
||||
Type ReadBigEndian(bytes::const_span data) {
|
||||
const auto bytes = data.subspan(0, sizeof(Type)).data();
|
||||
return qFromBigEndian(*reinterpret_cast<const Type*>(bytes));
|
||||
}
|
||||
|
||||
bool IsAtom(bytes::const_span header, const char (&atom)[5]) {
|
||||
return bytes::compare(
|
||||
header.subspan(4, 4),
|
||||
bytes::make_span(atom).subspan(0, 4)) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CheckStreamingSupport(
|
||||
const Core::FileLocation &location,
|
||||
QByteArray data) {
|
||||
QBuffer buffer;
|
||||
QFile file;
|
||||
if (data.isEmpty()) {
|
||||
file.setFileName(location.name());
|
||||
} else {
|
||||
buffer.setBuffer(&data);
|
||||
}
|
||||
const auto size = data.isEmpty()
|
||||
? file.size()
|
||||
: data.size();
|
||||
const auto device = data.isEmpty()
|
||||
? static_cast<QIODevice*>(&file)
|
||||
: static_cast<QIODevice*>(&buffer);
|
||||
|
||||
if (size < kHeaderSize || !device->open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lastReadPosition = 0;
|
||||
char atomHeader[kHeaderSize] = { 0 };
|
||||
auto atomHeaderBytes = bytes::make_span(atomHeader);
|
||||
while (true) {
|
||||
const auto position = device->pos();
|
||||
if (device->read(atomHeader, kHeaderSize) != kHeaderSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (lastReadPosition >= kFindMoovBefore) {
|
||||
return false;
|
||||
} else if (IsAtom(atomHeaderBytes, "moov")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto length = [&] {
|
||||
const auto result = ReadBigEndian<uint32>(atomHeaderBytes);
|
||||
if (result != 1) {
|
||||
return uint64(result);
|
||||
}
|
||||
char atomSize64[kHeaderSize] = { 0 };
|
||||
if (device->read(atomSize64, kHeaderSize) != kHeaderSize) {
|
||||
return uint64(-1);
|
||||
}
|
||||
auto atomSize64Bytes = bytes::make_span(atomSize64);
|
||||
return ReadBigEndian<uint64>(atomSize64Bytes);
|
||||
}();
|
||||
if (position + length > size) {
|
||||
break;
|
||||
}
|
||||
device->seek(position + length);
|
||||
lastReadPosition = position;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
22
Telegram/SourceFiles/media/clip/media_clip_check_streaming.h
Normal file
22
Telegram/SourceFiles/media/clip/media_clip_check_streaming.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Core {
|
||||
class FileLocation;
|
||||
} // namespace Core
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
|
||||
bool CheckStreamingSupport(
|
||||
const Core::FileLocation &location,
|
||||
QByteArray data);
|
||||
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
514
Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp
Normal file
514
Telegram/SourceFiles/media/clip/media_clip_ffmpeg.cpp
Normal file
@@ -0,0 +1,514 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/clip/media_clip_ffmpeg.h"
|
||||
|
||||
#include "core/file_location.h"
|
||||
#include "logs.h"
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
namespace internal {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSkipInvalidDataPackets = 10;
|
||||
constexpr auto kMaxInlineArea = 1280 * 720;
|
||||
constexpr auto kMaxSendingArea = 3840 * 2160; // usual 4K
|
||||
|
||||
// See https://github.com/telegramdesktop/tdesktop/issues/7225
|
||||
constexpr auto kAlignImageBy = 64;
|
||||
|
||||
void alignedImageBufferCleanupHandler(void *data) {
|
||||
auto buffer = static_cast<uchar*>(data);
|
||||
delete[] buffer;
|
||||
}
|
||||
|
||||
// Create a QImage of desired size where all the data is aligned to 16 bytes.
|
||||
QImage createAlignedImage(QSize size) {
|
||||
auto width = size.width();
|
||||
auto height = size.height();
|
||||
auto widthalign = kAlignImageBy / 4;
|
||||
auto neededwidth = width + ((width % widthalign) ? (widthalign - (width % widthalign)) : 0);
|
||||
auto bytesperline = neededwidth * 4;
|
||||
auto buffer = new uchar[bytesperline * height + kAlignImageBy];
|
||||
auto cleanupdata = static_cast<void*>(buffer);
|
||||
auto bufferval = reinterpret_cast<uintptr_t>(buffer);
|
||||
auto alignedbuffer = buffer + ((bufferval % kAlignImageBy) ? (kAlignImageBy - (bufferval % kAlignImageBy)) : 0);
|
||||
return QImage(alignedbuffer, width, height, bytesperline, QImage::Format_ARGB32_Premultiplied, alignedImageBufferCleanupHandler, cleanupdata);
|
||||
}
|
||||
|
||||
bool isAlignedImage(const QImage &image) {
|
||||
return !(reinterpret_cast<uintptr_t>(image.constBits()) % kAlignImageBy) && !(image.bytesPerLine() % kAlignImageBy);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FFMpegReaderImplementation::FFMpegReaderImplementation(
|
||||
Core::FileLocation *location,
|
||||
QByteArray *data)
|
||||
: ReaderImplementation(location, data)
|
||||
, _frame(FFmpeg::MakeFramePointer()) {
|
||||
}
|
||||
|
||||
ReaderImplementation::ReadResult FFMpegReaderImplementation::readNextFrame() {
|
||||
do {
|
||||
int res = avcodec_receive_frame(_codecContext, _frame.get());
|
||||
if (res >= 0) {
|
||||
const auto limit = (_mode == Mode::Inspecting)
|
||||
? kMaxSendingArea
|
||||
: kMaxInlineArea;
|
||||
if (_frame->width * _frame->height > limit) {
|
||||
return ReadResult::Error;
|
||||
}
|
||||
processReadFrame();
|
||||
return ReadResult::Success;
|
||||
}
|
||||
|
||||
if (res == AVERROR_EOF) {
|
||||
_packetQueue.clear();
|
||||
if (!_hadFrame) {
|
||||
LOG(("Gif Error: Got EOF before a single frame was read!"));
|
||||
return ReadResult::Error;
|
||||
}
|
||||
|
||||
if ((res = avformat_seek_file(_fmtContext, _streamId, std::numeric_limits<int64_t>::min(), 0, std::numeric_limits<int64_t>::max(), 0)) < 0) {
|
||||
if ((res = av_seek_frame(_fmtContext, _streamId, 0, AVSEEK_FLAG_BYTE)) < 0) {
|
||||
if ((res = av_seek_frame(_fmtContext, _streamId, 0, AVSEEK_FLAG_FRAME)) < 0) {
|
||||
if ((res = av_seek_frame(_fmtContext, _streamId, 0, 0)) < 0) {
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
LOG(("Gif Error: Unable to av_seek_frame() to the start %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return ReadResult::Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
avcodec_flush_buffers(_codecContext);
|
||||
_hadFrame = false;
|
||||
_frameMs = 0;
|
||||
_lastReadVideoMs = _lastReadAudioMs = 0;
|
||||
_skippedInvalidDataPackets = 0;
|
||||
_frameIndex = -1;
|
||||
|
||||
continue;
|
||||
} else if (res != AVERROR(EAGAIN)) {
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
LOG(("Gif Error: Unable to avcodec_receive_frame() %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return ReadResult::Error;
|
||||
}
|
||||
|
||||
while (_packetQueue.empty()) {
|
||||
auto packetResult = readAndProcessPacket();
|
||||
if (packetResult == PacketResult::Error) {
|
||||
return ReadResult::Error;
|
||||
} else if (packetResult == PacketResult::EndOfFile) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_packetQueue.empty()) {
|
||||
avcodec_send_packet(_codecContext, nullptr); // drain
|
||||
continue;
|
||||
}
|
||||
|
||||
auto packet = std::move(_packetQueue.front());
|
||||
_packetQueue.pop_front();
|
||||
|
||||
const auto native = &packet.fields();
|
||||
const auto guard = gsl::finally([
|
||||
&,
|
||||
size = native->size,
|
||||
data = native->data
|
||||
] {
|
||||
native->size = size;
|
||||
native->data = data;
|
||||
packet = FFmpeg::Packet();
|
||||
});
|
||||
|
||||
res = avcodec_send_packet(_codecContext, native);
|
||||
if (res < 0) {
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
LOG(("Gif Error: Unable to avcodec_send_packet() %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
if (res == AVERROR_INVALIDDATA) {
|
||||
if (++_skippedInvalidDataPackets < kSkipInvalidDataPackets) {
|
||||
continue; // try to skip bad packet
|
||||
}
|
||||
}
|
||||
return ReadResult::Error;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
return ReadResult::Error;
|
||||
}
|
||||
|
||||
void FFMpegReaderImplementation::processReadFrame() {
|
||||
int64 duration = _frame->duration;
|
||||
int64 framePts = _frame->pts;
|
||||
crl::time frameMs = (framePts * 1000LL * _fmtContext->streams[_streamId]->time_base.num) / _fmtContext->streams[_streamId]->time_base.den;
|
||||
_currentFrameDelay = _nextFrameDelay;
|
||||
if (_frameMs + _currentFrameDelay < frameMs) {
|
||||
_currentFrameDelay = int32(frameMs - _frameMs);
|
||||
} else if (frameMs < _frameMs + _currentFrameDelay) {
|
||||
frameMs = _frameMs + _currentFrameDelay;
|
||||
}
|
||||
|
||||
if (duration == AV_NOPTS_VALUE) {
|
||||
_nextFrameDelay = 0;
|
||||
} else {
|
||||
_nextFrameDelay = (duration * 1000LL * _fmtContext->streams[_streamId]->time_base.num) / _fmtContext->streams[_streamId]->time_base.den;
|
||||
}
|
||||
_frameMs = frameMs;
|
||||
|
||||
_hadFrame = _frameRead = true;
|
||||
_frameTime += _currentFrameDelay;
|
||||
++_frameIndex;
|
||||
}
|
||||
|
||||
ReaderImplementation::ReadResult FFMpegReaderImplementation::readFramesTill(crl::time frameMs, crl::time systemMs) {
|
||||
if (_frameRead && _frameTime > frameMs) {
|
||||
return ReadResult::Success;
|
||||
}
|
||||
auto readResult = readNextFrame();
|
||||
if (readResult != ReadResult::Success || _frameTime > frameMs) {
|
||||
return readResult;
|
||||
}
|
||||
readResult = readNextFrame();
|
||||
if (_frameTime <= frameMs) {
|
||||
_frameTime = frameMs + 5; // keep up
|
||||
}
|
||||
return readResult;
|
||||
}
|
||||
|
||||
crl::time FFMpegReaderImplementation::frameRealTime() const {
|
||||
return _frameMs;
|
||||
}
|
||||
|
||||
crl::time FFMpegReaderImplementation::framePresentationTime() const {
|
||||
return qMax(_frameTime + _frameTimeCorrection, crl::time(0));
|
||||
}
|
||||
|
||||
crl::time FFMpegReaderImplementation::durationMs() const {
|
||||
const auto rebase = [](int64_t duration, const AVRational &base) {
|
||||
return (duration * 1000LL * base.num) / base.den;
|
||||
};
|
||||
const auto stream = _fmtContext->streams[_streamId];
|
||||
if (stream->duration != AV_NOPTS_VALUE) {
|
||||
return rebase(stream->duration, stream->time_base);
|
||||
} else if (_fmtContext->duration != AV_NOPTS_VALUE) {
|
||||
return rebase(_fmtContext->duration, AVRational{ 1, AV_TIME_BASE });
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool FFMpegReaderImplementation::renderFrame(
|
||||
QImage &to,
|
||||
bool &hasAlpha,
|
||||
int &index,
|
||||
const QSize &size) {
|
||||
Expects(_frameRead);
|
||||
|
||||
_frameRead = false;
|
||||
index = _frameIndex;
|
||||
if (!_width || !_height) {
|
||||
_width = _frame->width;
|
||||
_height = _frame->height;
|
||||
if (!_width || !_height) {
|
||||
LOG(("Gif Error: Bad frame size %1").arg(logData()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
QSize toSize(size.isEmpty() ? QSize(_width, _height) : size);
|
||||
if (!size.isEmpty() && rotationSwapWidthHeight()) {
|
||||
toSize.transpose();
|
||||
}
|
||||
if (to.isNull() || to.size() != toSize || !to.isDetached() || !isAlignedImage(to)) {
|
||||
to = createAlignedImage(toSize);
|
||||
}
|
||||
const auto format = (_frame->format == AV_PIX_FMT_NONE)
|
||||
? _codecContext->pix_fmt
|
||||
: _frame->format;
|
||||
const auto bgra = (format == AV_PIX_FMT_BGRA);
|
||||
hasAlpha = bgra || (format == AV_PIX_FMT_YUVA420P);
|
||||
if (_frame->width == toSize.width() && _frame->height == toSize.height() && bgra) {
|
||||
int32 sbpl = _frame->linesize[0], dbpl = to.bytesPerLine(), bpl = qMin(sbpl, dbpl);
|
||||
uchar *s = _frame->data[0], *d = to.bits();
|
||||
for (int32 i = 0, l = _frame->height; i < l; ++i) {
|
||||
memcpy(d + i * dbpl, s + i * sbpl, bpl);
|
||||
}
|
||||
} else {
|
||||
if ((_swsSize != toSize) || (_frame->format != -1 && _frame->format != _codecContext->pix_fmt) || !_swsContext) {
|
||||
_swsSize = toSize;
|
||||
_swsContext = sws_getCachedContext(_swsContext, _frame->width, _frame->height, AVPixelFormat(_frame->format), toSize.width(), toSize.height(), AV_PIX_FMT_BGRA, 0, nullptr, nullptr, nullptr);
|
||||
}
|
||||
// AV_NUM_DATA_POINTERS defined in AVFrame struct
|
||||
uint8_t *toData[AV_NUM_DATA_POINTERS] = { to.bits(), nullptr };
|
||||
int toLinesize[AV_NUM_DATA_POINTERS] = { int(to.bytesPerLine()), 0 };
|
||||
sws_scale(_swsContext, _frame->data, _frame->linesize, 0, _frame->height, toData, toLinesize);
|
||||
}
|
||||
if (hasAlpha) {
|
||||
FFmpeg::PremultiplyInplace(to);
|
||||
}
|
||||
if (_rotation != Rotation::None) {
|
||||
QTransform rotationTransform;
|
||||
switch (_rotation) {
|
||||
case Rotation::Degrees90: rotationTransform.rotate(90); break;
|
||||
case Rotation::Degrees180: rotationTransform.rotate(180); break;
|
||||
case Rotation::Degrees270: rotationTransform.rotate(270); break;
|
||||
}
|
||||
to = to.transformed(rotationTransform);
|
||||
}
|
||||
|
||||
FFmpeg::ClearFrameMemory(_frame.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FFMpegReaderImplementation::Rotation FFMpegReaderImplementation::rotationFromDegrees(int degrees) const {
|
||||
switch (degrees) {
|
||||
case 90: return Rotation::Degrees90;
|
||||
case 180: return Rotation::Degrees180;
|
||||
case 270: return Rotation::Degrees270;
|
||||
}
|
||||
return Rotation::None;
|
||||
}
|
||||
|
||||
bool FFMpegReaderImplementation::start(Mode mode, crl::time &positionMs) {
|
||||
_mode = mode;
|
||||
|
||||
initDevice();
|
||||
if (!_device->open(QIODevice::ReadOnly)) {
|
||||
LOG(("Gif Error: Unable to open device %1").arg(logData()));
|
||||
return false;
|
||||
}
|
||||
_ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);
|
||||
_ioContext = avio_alloc_context(_ioBuffer, FFmpeg::kAVBlockSize, 0, static_cast<void*>(this), &FFMpegReaderImplementation::Read, nullptr, &FFMpegReaderImplementation::Seek);
|
||||
_fmtContext = avformat_alloc_context();
|
||||
if (!_fmtContext) {
|
||||
LOG(("Gif Error: Unable to avformat_alloc_context %1").arg(logData()));
|
||||
return false;
|
||||
}
|
||||
_fmtContext->pb = _ioContext;
|
||||
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
if ((res = avformat_open_input(&_fmtContext, nullptr, nullptr, nullptr)) < 0) {
|
||||
_ioBuffer = nullptr;
|
||||
|
||||
LOG(("Gif Error: Unable to avformat_open_input %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return false;
|
||||
}
|
||||
_opened = true;
|
||||
|
||||
if ((res = avformat_find_stream_info(_fmtContext, nullptr)) < 0) {
|
||||
LOG(("Gif Error: Unable to avformat_find_stream_info %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return false;
|
||||
}
|
||||
|
||||
_streamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
|
||||
if (_streamId < 0) {
|
||||
LOG(("Gif Error: Unable to av_find_best_stream %1, error %2, %3").arg(logData()).arg(_streamId).arg(av_make_error_string(err, sizeof(err), _streamId)));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto rotateTag = av_dict_get(_fmtContext->streams[_streamId]->metadata, "rotate", nullptr, 0);
|
||||
if (rotateTag && *rotateTag->value) {
|
||||
auto stringRotateTag = QString::fromUtf8(rotateTag->value);
|
||||
auto toIntSucceeded = false;
|
||||
auto rotateDegrees = stringRotateTag.toInt(&toIntSucceeded);
|
||||
if (toIntSucceeded) {
|
||||
_rotation = rotationFromDegrees(rotateDegrees);
|
||||
}
|
||||
}
|
||||
|
||||
_codecContext = avcodec_alloc_context3(nullptr);
|
||||
if (!_codecContext) {
|
||||
LOG(("Gif Error: Unable to avcodec_alloc_context3 %1").arg(logData()));
|
||||
return false;
|
||||
}
|
||||
if ((res = avcodec_parameters_to_context(_codecContext, _fmtContext->streams[_streamId]->codecpar)) < 0) {
|
||||
LOG(("Gif Error: Unable to avcodec_parameters_to_context %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return false;
|
||||
}
|
||||
_codecContext->pkt_timebase = _fmtContext->streams[_streamId]->time_base;
|
||||
av_opt_set_int(_codecContext, "refcounted_frames", 1, 0);
|
||||
|
||||
const auto codec = FFmpeg::FindDecoder(_codecContext);
|
||||
if (!codec) {
|
||||
LOG(("Gif Error: Unable to avcodec_find_decoder %1").arg(logData()));
|
||||
return false;
|
||||
}
|
||||
if (_mode == Mode::Inspecting) {
|
||||
const auto audioStreamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
|
||||
_hasAudioStream = (audioStreamId >= 0);
|
||||
}
|
||||
|
||||
if ((res = avcodec_open2(_codecContext, codec, nullptr)) < 0) {
|
||||
LOG(("Gif Error: Unable to avcodec_open2 %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (positionMs > 0) {
|
||||
const auto timeBase = _fmtContext->streams[_streamId]->time_base;
|
||||
const auto timeStamp = (positionMs * timeBase.den)
|
||||
/ (1000LL * timeBase.num);
|
||||
if (av_seek_frame(_fmtContext, _streamId, timeStamp, 0) < 0) {
|
||||
if (av_seek_frame(_fmtContext, _streamId, timeStamp, AVSEEK_FLAG_BACKWARD) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FFmpeg::Packet packet;
|
||||
auto readResult = readPacket(packet);
|
||||
if (readResult == PacketResult::Ok && positionMs > 0) {
|
||||
positionMs = countPacketMs(packet);
|
||||
}
|
||||
|
||||
if (readResult == PacketResult::Ok) {
|
||||
processPacket(std::move(packet));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FFMpegReaderImplementation::inspectAt(crl::time &positionMs) {
|
||||
if (positionMs > 0) {
|
||||
const auto timeBase = _fmtContext->streams[_streamId]->time_base;
|
||||
const auto timeStamp = (positionMs * timeBase.den)
|
||||
/ (1000LL * timeBase.num);
|
||||
if (av_seek_frame(_fmtContext, _streamId, timeStamp, 0) < 0) {
|
||||
if (av_seek_frame(_fmtContext, _streamId, timeStamp, AVSEEK_FLAG_BACKWARD) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_packetQueue.clear();
|
||||
|
||||
FFmpeg::Packet packet;
|
||||
auto readResult = readPacket(packet);
|
||||
if (readResult == PacketResult::Ok && positionMs > 0) {
|
||||
positionMs = countPacketMs(packet);
|
||||
}
|
||||
|
||||
if (readResult == PacketResult::Ok) {
|
||||
processPacket(std::move(packet));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FFMpegReaderImplementation::isGifv() const {
|
||||
if (_hasAudioStream) {
|
||||
return false;
|
||||
}
|
||||
if (dataSize() > kMaxInMemory) {
|
||||
return false;
|
||||
}
|
||||
if (_codecContext->codec_id != AV_CODEC_ID_H264) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FFMpegReaderImplementation::isWebmSticker() const {
|
||||
if (_hasAudioStream) {
|
||||
return false;
|
||||
}
|
||||
if (dataSize() > kMaxInMemory) {
|
||||
return false;
|
||||
}
|
||||
if (_codecContext->codec_id != AV_CODEC_ID_VP9) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString FFMpegReaderImplementation::logData() const {
|
||||
return u"for file '%1', data size '%2'"_q.arg(_location ? _location->name() : QString()).arg(_data->size());
|
||||
}
|
||||
|
||||
FFMpegReaderImplementation::~FFMpegReaderImplementation() {
|
||||
if (_codecContext) avcodec_free_context(&_codecContext);
|
||||
if (_swsContext) sws_freeContext(_swsContext);
|
||||
if (_opened) {
|
||||
avformat_close_input(&_fmtContext);
|
||||
}
|
||||
if (_ioContext) {
|
||||
av_freep(&_ioContext->buffer);
|
||||
av_freep(&_ioContext);
|
||||
} else if (_ioBuffer) {
|
||||
av_freep(&_ioBuffer);
|
||||
}
|
||||
if (_fmtContext) avformat_free_context(_fmtContext);
|
||||
}
|
||||
|
||||
FFMpegReaderImplementation::PacketResult FFMpegReaderImplementation::readPacket(FFmpeg::Packet &packet) {
|
||||
int res = 0;
|
||||
if ((res = av_read_frame(_fmtContext, &packet.fields())) < 0) {
|
||||
if (res == AVERROR_EOF) {
|
||||
return PacketResult::EndOfFile;
|
||||
}
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
LOG(("Gif Error: Unable to av_read_frame() %1, error %2, %3").arg(logData()).arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return PacketResult::Error;
|
||||
}
|
||||
return PacketResult::Ok;
|
||||
}
|
||||
|
||||
void FFMpegReaderImplementation::processPacket(FFmpeg::Packet &&packet) {
|
||||
const auto &native = packet.fields();
|
||||
auto videoPacket = (native.stream_index == _streamId);
|
||||
if (videoPacket) {
|
||||
_lastReadVideoMs = countPacketMs(packet);
|
||||
_packetQueue.push_back(std::move(packet));
|
||||
}
|
||||
}
|
||||
|
||||
crl::time FFMpegReaderImplementation::countPacketMs(
|
||||
const FFmpeg::Packet &packet) const {
|
||||
const auto &native = packet.fields();
|
||||
int64 packetPts = (native.pts == AV_NOPTS_VALUE) ? native.dts : native.pts;
|
||||
crl::time packetMs = (packetPts * 1000LL * _fmtContext->streams[native.stream_index]->time_base.num) / _fmtContext->streams[native.stream_index]->time_base.den;
|
||||
return packetMs;
|
||||
}
|
||||
|
||||
FFMpegReaderImplementation::PacketResult FFMpegReaderImplementation::readAndProcessPacket() {
|
||||
FFmpeg::Packet packet;
|
||||
auto result = readPacket(packet);
|
||||
if (result == PacketResult::Ok) {
|
||||
processPacket(std::move(packet));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int FFMpegReaderImplementation::Read(void *opaque, uint8_t *buf, int buf_size) {
|
||||
FFMpegReaderImplementation *l = reinterpret_cast<FFMpegReaderImplementation*>(opaque);
|
||||
int ret = l->_device->read((char*)(buf), buf_size);
|
||||
switch (ret) {
|
||||
case -1: return AVERROR_EXTERNAL;
|
||||
case 0: return AVERROR_EOF;
|
||||
default: return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t FFMpegReaderImplementation::Seek(void *opaque, int64_t offset, int whence) {
|
||||
FFMpegReaderImplementation *l = reinterpret_cast<FFMpegReaderImplementation*>(opaque);
|
||||
|
||||
switch (whence) {
|
||||
case SEEK_SET: return l->_device->seek(offset) ? l->_device->pos() : -1;
|
||||
case SEEK_CUR: return l->_device->seek(l->_device->pos() + offset) ? l->_device->pos() : -1;
|
||||
case SEEK_END: return l->_device->seek(l->_device->size() + offset) ? l->_device->pos() : -1;
|
||||
case AVSEEK_SIZE: {
|
||||
// Special whence for determining filesize without any seek.
|
||||
return l->_dataSize;
|
||||
} break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
120
Telegram/SourceFiles/media/clip/media_clip_ffmpeg.h
Normal file
120
Telegram/SourceFiles/media/clip/media_clip_ffmpeg.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/clip/media_clip_implementation.h"
|
||||
#include "ffmpeg/ffmpeg_utility.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libavutil/opt.h>
|
||||
} // extern "C"
|
||||
#include <deque>
|
||||
|
||||
//#include "media/streaming/media_streaming_utility.h"
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
namespace internal {
|
||||
|
||||
constexpr auto kMaxInMemory = 10 * 1024 * 1024;
|
||||
|
||||
class FFMpegReaderImplementation : public ReaderImplementation {
|
||||
public:
|
||||
FFMpegReaderImplementation(Core::FileLocation *location, QByteArray *data);
|
||||
|
||||
ReadResult readFramesTill(crl::time frameMs, crl::time systemMs) override;
|
||||
|
||||
crl::time frameRealTime() const override;
|
||||
crl::time framePresentationTime() const override;
|
||||
|
||||
bool renderFrame(
|
||||
QImage &to,
|
||||
bool &hasAlpha,
|
||||
int &index,
|
||||
const QSize &size) override;
|
||||
|
||||
crl::time durationMs() const override;
|
||||
|
||||
bool start(Mode mode, crl::time &positionMs) override;
|
||||
bool inspectAt(crl::time &positionMs);
|
||||
|
||||
QString logData() const;
|
||||
|
||||
bool isGifv() const;
|
||||
bool isWebmSticker() const;
|
||||
|
||||
~FFMpegReaderImplementation();
|
||||
|
||||
private:
|
||||
ReadResult readNextFrame();
|
||||
void processReadFrame();
|
||||
|
||||
enum class PacketResult {
|
||||
Ok,
|
||||
EndOfFile,
|
||||
Error,
|
||||
};
|
||||
PacketResult readPacket(FFmpeg::Packet &packet);
|
||||
void processPacket(FFmpeg::Packet &&packet);
|
||||
crl::time countPacketMs(const FFmpeg::Packet &packet) const;
|
||||
PacketResult readAndProcessPacket();
|
||||
|
||||
enum class Rotation {
|
||||
None,
|
||||
Degrees90,
|
||||
Degrees180,
|
||||
Degrees270,
|
||||
};
|
||||
Rotation rotationFromDegrees(int degrees) const;
|
||||
bool rotationSwapWidthHeight() const {
|
||||
return (_rotation == Rotation::Degrees90) || (_rotation == Rotation::Degrees270);
|
||||
}
|
||||
|
||||
static int Read(void *opaque, uint8_t *buf, int buf_size);
|
||||
static int64_t Seek(void *opaque, int64_t offset, int whence);
|
||||
|
||||
Mode _mode = Mode::Silent;
|
||||
|
||||
Rotation _rotation = Rotation::None;
|
||||
|
||||
uchar *_ioBuffer = nullptr;
|
||||
AVIOContext *_ioContext = nullptr;
|
||||
AVFormatContext *_fmtContext = nullptr;
|
||||
AVCodecContext *_codecContext = nullptr;
|
||||
int _streamId = 0;
|
||||
FFmpeg::FramePointer _frame;
|
||||
int _frameIndex = -1;
|
||||
bool _opened = false;
|
||||
bool _hadFrame = false;
|
||||
bool _frameRead = false;
|
||||
int _skippedInvalidDataPackets = 0;
|
||||
|
||||
bool _hasAudioStream = false;
|
||||
crl::time _lastReadVideoMs = 0;
|
||||
crl::time _lastReadAudioMs = 0;
|
||||
|
||||
std::deque<FFmpeg::Packet> _packetQueue;
|
||||
|
||||
int _width = 0;
|
||||
int _height = 0;
|
||||
SwsContext *_swsContext = nullptr;
|
||||
QSize _swsSize;
|
||||
|
||||
crl::time _frameMs = 0;
|
||||
int _nextFrameDelay = 0;
|
||||
int _currentFrameDelay = 0;
|
||||
|
||||
crl::time _frameTime = 0;
|
||||
crl::time _frameTimeCorrection = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/clip/media_clip_implementation.h"
|
||||
|
||||
#include "core/file_location.h"
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
namespace internal {
|
||||
|
||||
void ReaderImplementation::initDevice() {
|
||||
if (_data->isEmpty()) {
|
||||
if (_file.isOpen()) _file.close();
|
||||
_file.setFileName(_location->name());
|
||||
_dataSize = _file.size();
|
||||
} else {
|
||||
if (_buffer.isOpen()) _buffer.close();
|
||||
_buffer.setBuffer(_data);
|
||||
_dataSize = _data->size();
|
||||
}
|
||||
_device = _data->isEmpty() ? static_cast<QIODevice*>(&_file) : static_cast<QIODevice*>(&_buffer);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
75
Telegram/SourceFiles/media/clip/media_clip_implementation.h
Normal file
75
Telegram/SourceFiles/media/clip/media_clip_implementation.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtCore/QFile>
|
||||
|
||||
namespace Core {
|
||||
class FileLocation;
|
||||
} // namespace Core
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
namespace internal {
|
||||
|
||||
class ReaderImplementation {
|
||||
public:
|
||||
ReaderImplementation(Core::FileLocation *location, QByteArray *data)
|
||||
: _location(location)
|
||||
, _data(data) {
|
||||
}
|
||||
enum class Mode {
|
||||
Silent,
|
||||
Inspecting, // Not playing video, but reading data.
|
||||
};
|
||||
|
||||
enum class ReadResult {
|
||||
Success,
|
||||
Error,
|
||||
EndOfFile,
|
||||
};
|
||||
// Read frames till current frame will have presentation time > frameMs, systemMs = crl::now().
|
||||
virtual ReadResult readFramesTill(crl::time frameMs, crl::time systemMs) = 0;
|
||||
|
||||
// Get current frame real and presentation time.
|
||||
virtual crl::time frameRealTime() const = 0;
|
||||
virtual crl::time framePresentationTime() const = 0;
|
||||
|
||||
// Render current frame to an image with specific size.
|
||||
virtual bool renderFrame(
|
||||
QImage &to,
|
||||
bool &hasAlpha,
|
||||
int &index,
|
||||
const QSize &size) = 0;
|
||||
|
||||
virtual crl::time durationMs() const = 0;
|
||||
|
||||
virtual bool start(Mode mode, crl::time &positionMs) = 0;
|
||||
|
||||
virtual ~ReaderImplementation() {
|
||||
}
|
||||
int64 dataSize() const {
|
||||
return _dataSize;
|
||||
}
|
||||
|
||||
protected:
|
||||
Core::FileLocation *_location = nullptr;
|
||||
QByteArray *_data = nullptr;
|
||||
QFile _file;
|
||||
QBuffer _buffer;
|
||||
QIODevice *_device = nullptr;
|
||||
int64 _dataSize = 0;
|
||||
|
||||
void initDevice();
|
||||
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
1022
Telegram/SourceFiles/media/clip/media_clip_reader.cpp
Normal file
1022
Telegram/SourceFiles/media/clip/media_clip_reader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
249
Telegram/SourceFiles/media/clip/media_clip_reader.h
Normal file
249
Telegram/SourceFiles/media/clip/media_clip_reader.h
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/image/image_prepare.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QMutex>
|
||||
|
||||
namespace Ui {
|
||||
struct PreparedFileInformation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Core {
|
||||
class FileLocation;
|
||||
} // namespace Core
|
||||
|
||||
namespace Media {
|
||||
namespace Clip {
|
||||
|
||||
enum class State {
|
||||
Reading,
|
||||
Error,
|
||||
Finished,
|
||||
};
|
||||
|
||||
struct FrameRequest {
|
||||
[[nodiscard]] bool valid() const {
|
||||
return factor > 0;
|
||||
}
|
||||
|
||||
QSize frame;
|
||||
QSize outer;
|
||||
int factor = 0;
|
||||
ImageRoundRadius radius = ImageRoundRadius::None;
|
||||
RectParts corners = RectPart::AllCorners;
|
||||
QColor colored = QColor(0, 0, 0, 0);
|
||||
bool keepAlpha = false;
|
||||
};
|
||||
|
||||
// Before ReaderPrivate read the first image and got the original frame size.
|
||||
inline constexpr auto kWaitingForDimensionsStep = -3;
|
||||
|
||||
// Before Reader got the original frame size and prepared the frame request.
|
||||
inline constexpr auto kWaitingForRequestStep = -2;
|
||||
|
||||
// Before ReaderPrivate got the frame request
|
||||
// and started waiting for the 1-2 delay.
|
||||
inline constexpr auto kWaitingForFirstFrameStep = -1;
|
||||
|
||||
enum class Notification {
|
||||
Reinit,
|
||||
Repaint,
|
||||
};
|
||||
|
||||
class Manager;
|
||||
class ReaderPrivate;
|
||||
class Reader {
|
||||
public:
|
||||
using Callback = Fn<void(Notification)>;
|
||||
enum class Mode {
|
||||
Gif,
|
||||
Video,
|
||||
};
|
||||
|
||||
Reader(const Core::FileLocation &location, const QByteArray &data, Callback &&callback);
|
||||
Reader(const QString &filepath, Callback &&callback);
|
||||
Reader(const QByteArray &data, Callback &&callback);
|
||||
|
||||
// Reader can be already deleted.
|
||||
static void SafeCallback(
|
||||
Reader *reader,
|
||||
int threadIndex,
|
||||
Notification notification);
|
||||
|
||||
void start(FrameRequest request);
|
||||
|
||||
struct FrameInfo {
|
||||
QImage image;
|
||||
int index = 0;
|
||||
};
|
||||
[[nodiscard]] FrameInfo frameInfo(FrameRequest request, crl::time now);
|
||||
[[nodiscard]] QImage current(FrameRequest request, crl::time now) {
|
||||
auto result = frameInfo(request, now).image;
|
||||
moveToNextFrame();
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] QImage frameOriginal() const {
|
||||
if (const auto frame = frameToShow()) {
|
||||
auto result = frame->original;
|
||||
result.detach();
|
||||
return result;
|
||||
}
|
||||
return QImage();
|
||||
}
|
||||
bool moveToNextFrame() {
|
||||
return moveToNextShow();
|
||||
}
|
||||
[[nodiscard]] bool currentDisplayed() const {
|
||||
const auto frame = frameToShow();
|
||||
return !frame || (frame->displayed.loadAcquire() != 0);
|
||||
}
|
||||
[[nodiscard]] bool autoPausedGif() const {
|
||||
return _autoPausedGif.loadAcquire();
|
||||
}
|
||||
[[nodiscard]] bool videoPaused() const;
|
||||
[[nodiscard]] int threadIndex() const {
|
||||
return _threadIndex;
|
||||
}
|
||||
|
||||
[[nodiscard]] int width() const;
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
[[nodiscard]] State state() const;
|
||||
[[nodiscard]] bool started() const {
|
||||
const auto step = _step.loadAcquire();
|
||||
return (step == kWaitingForFirstFrameStep) || (step >= 0);
|
||||
}
|
||||
[[nodiscard]] bool ready() const;
|
||||
|
||||
[[nodiscard]] crl::time getPositionMs() const;
|
||||
[[nodiscard]] crl::time getDurationMs() const;
|
||||
void pauseResumeVideo();
|
||||
|
||||
void stop();
|
||||
void error();
|
||||
void finished();
|
||||
|
||||
~Reader();
|
||||
|
||||
private:
|
||||
void init(const Core::FileLocation &location, const QByteArray &data);
|
||||
|
||||
Callback _callback;
|
||||
State _state = State::Reading;
|
||||
|
||||
crl::time _durationMs = 0;
|
||||
|
||||
mutable int _width = 0;
|
||||
mutable int _height = 0;
|
||||
|
||||
// -2, -1 - init, 0-5 - work, show ((state + 1) / 2) % 3 state, write ((state + 3) / 2) % 3
|
||||
mutable QAtomicInt _step = kWaitingForDimensionsStep;
|
||||
struct Frame {
|
||||
void clear() {
|
||||
prepared = QImage();
|
||||
preparedColored = QColor(0, 0, 0, 0);
|
||||
original = QImage();
|
||||
}
|
||||
|
||||
QImage prepared;
|
||||
QColor preparedColored = QColor(0, 0, 0, 0);
|
||||
QImage original;
|
||||
FrameRequest request;
|
||||
QAtomicInt displayed = 0;
|
||||
int index = 0;
|
||||
|
||||
// Should be counted from the end,
|
||||
// so that positionMs <= _durationMs.
|
||||
crl::time positionMs = 0;
|
||||
};
|
||||
mutable Frame _frames[3];
|
||||
Frame *frameToShow(int *index = nullptr) const; // 0 means not ready
|
||||
Frame *frameToWrite(int *index = nullptr) const; // 0 means not ready
|
||||
Frame *frameToWriteNext(bool check, int *index = nullptr) const;
|
||||
bool moveToNextShow() const;
|
||||
void moveToNextWrite() const;
|
||||
|
||||
QAtomicInt _autoPausedGif = 0;
|
||||
QAtomicInt _videoPauseRequest = 0;
|
||||
int32 _threadIndex;
|
||||
|
||||
friend class Manager;
|
||||
|
||||
ReaderPrivate *_private = nullptr;
|
||||
|
||||
};
|
||||
|
||||
class ReaderPointer {
|
||||
public:
|
||||
ReaderPointer(std::nullptr_t = nullptr) {
|
||||
}
|
||||
explicit ReaderPointer(Reader *pointer) : _pointer(pointer) {
|
||||
}
|
||||
ReaderPointer(const ReaderPointer &other) = delete;
|
||||
ReaderPointer &operator=(const ReaderPointer &other) = delete;
|
||||
ReaderPointer(ReaderPointer &&other) : _pointer(base::take(other._pointer)) {
|
||||
}
|
||||
ReaderPointer &operator=(ReaderPointer &&other) {
|
||||
swap(other);
|
||||
return *this;
|
||||
}
|
||||
void swap(ReaderPointer &other) {
|
||||
qSwap(_pointer, other._pointer);
|
||||
}
|
||||
Reader *get() const {
|
||||
return valid() ? _pointer : nullptr;
|
||||
}
|
||||
Reader *operator->() const {
|
||||
return get();
|
||||
}
|
||||
void setBad() {
|
||||
reset();
|
||||
_pointer = BadPointer;
|
||||
}
|
||||
void reset() {
|
||||
ReaderPointer temp;
|
||||
swap(temp);
|
||||
}
|
||||
bool isBad() const {
|
||||
return (_pointer == BadPointer);
|
||||
}
|
||||
bool valid() const {
|
||||
return _pointer && !isBad();
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return valid();
|
||||
}
|
||||
static inline ReaderPointer Bad() {
|
||||
ReaderPointer result;
|
||||
result.setBad();
|
||||
return result;
|
||||
}
|
||||
~ReaderPointer();
|
||||
|
||||
private:
|
||||
Reader *_pointer = nullptr;
|
||||
static Reader *const BadPointer;
|
||||
|
||||
};
|
||||
|
||||
template <typename ...Args>
|
||||
inline ReaderPointer MakeReader(Args&&... args) {
|
||||
return ReaderPointer(new Reader(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::PreparedFileInformation PrepareForSending(
|
||||
const QString &fname,
|
||||
const QByteArray &data);
|
||||
|
||||
void Finish();
|
||||
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
Reference in New Issue
Block a user