mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 22:54:01 +02:00
Concatenate two recordings.
This commit is contained in:
parent
d7ffdbd78d
commit
4142ada729
3 changed files with 219 additions and 22 deletions
|
@ -1688,7 +1688,10 @@ void VoiceRecordBar::startRecording() {
|
|||
instance()->pause(false, nullptr);
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->resume({
|
||||
.content = _data.bytes,
|
||||
.video = {
|
||||
.content = _data.bytes,
|
||||
.duration = _data.duration,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -32,6 +32,39 @@ constexpr auto kInitTimeout = 5 * crl::time(1000);
|
|||
|
||||
using namespace FFmpeg;
|
||||
|
||||
struct ReadBytesWrap {
|
||||
int64 size = 0;
|
||||
int64 offset = 0;
|
||||
const uchar *data = nullptr;
|
||||
|
||||
static int Read(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto wrap = static_cast<ReadBytesWrap*>(opaque);
|
||||
const auto toRead = std::min(
|
||||
int64(buf_size),
|
||||
wrap->size - wrap->offset);
|
||||
if (toRead > 0) {
|
||||
memcpy(buf, wrap->data + wrap->offset, toRead);
|
||||
wrap->offset += toRead;
|
||||
}
|
||||
return toRead;
|
||||
};
|
||||
static int64 Seek(void *opaque, int64_t offset, int whence) {
|
||||
auto wrap = static_cast<ReadBytesWrap*>(opaque);
|
||||
auto updated = int64(-1);
|
||||
switch (whence) {
|
||||
case SEEK_SET: updated = offset; break;
|
||||
case SEEK_CUR: updated = wrap->offset + offset; break;
|
||||
case SEEK_END: updated = wrap->size + offset; break;
|
||||
case AVSEEK_SIZE: return wrap->size; break;
|
||||
}
|
||||
if (updated < 0 || updated > wrap->size) {
|
||||
return -1;
|
||||
}
|
||||
wrap->offset = updated;
|
||||
return updated;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class RoundVideoRecorder::Private final {
|
||||
|
@ -49,8 +82,21 @@ public:
|
|||
void restart(RoundVideoPartial partial);
|
||||
|
||||
private:
|
||||
static int Write(void *opaque, uint8_t *buf, int buf_size);
|
||||
static int64_t Seek(void *opaque, int64_t offset, int whence);
|
||||
static constexpr auto kMaxStreams = 2;
|
||||
|
||||
struct CopyContext {
|
||||
CopyContext();
|
||||
|
||||
std::array<int64, kMaxStreams> lastPts = { 0 };
|
||||
std::array<int64, kMaxStreams> lastDts = { 0 };
|
||||
};
|
||||
|
||||
static int Write(void *opaque, uint8_t *buf, int buf_size) {
|
||||
return static_cast<Private*>(opaque)->write(buf, buf_size);
|
||||
}
|
||||
static int64_t Seek(void *opaque, int64_t offset, int whence) {
|
||||
return static_cast<Private*>(opaque)->seek(offset, whence);
|
||||
}
|
||||
|
||||
int write(uint8_t *buf, int buf_size);
|
||||
int64_t seek(int64_t offset, int whence);
|
||||
|
@ -77,6 +123,16 @@ private:
|
|||
void cutCircleFromYUV420P(not_null<AVFrame*> frame);
|
||||
void initCircleMask();
|
||||
|
||||
[[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video);
|
||||
[[nodiscard]] static FormatPointer OpenInputContext(
|
||||
not_null<const QByteArray*> data,
|
||||
not_null<ReadBytesWrap*> wrap);
|
||||
[[nodiscard]] bool copyPackets(
|
||||
not_null<AVFormatContext*> input,
|
||||
not_null<AVFormatContext*> output,
|
||||
CopyContext &context,
|
||||
crl::time offset = 0);
|
||||
|
||||
const crl::weak_on_queue<Private> _weak;
|
||||
|
||||
FormatPointer _format;
|
||||
|
@ -113,8 +169,9 @@ private:
|
|||
rpl::event_stream<Update, Error> _updates;
|
||||
|
||||
crl::time _maxDuration = 0;
|
||||
crl::time _previousPartsDuration = 0;
|
||||
QByteArray _previousContent;
|
||||
RoundVideoResult _previous;
|
||||
|
||||
ReadBytesWrap _forConcat1, _forConcat2;
|
||||
|
||||
std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D
|
||||
|
||||
|
@ -122,6 +179,11 @@ private:
|
|||
|
||||
};
|
||||
|
||||
RoundVideoRecorder::Private::CopyContext::CopyContext() {
|
||||
ranges::fill(lastPts, std::numeric_limits<int64>::min());
|
||||
ranges::fill(lastDts, std::numeric_limits<int64>::min());
|
||||
}
|
||||
|
||||
RoundVideoRecorder::Private::Private(crl::weak_on_queue<Private> weak)
|
||||
: _weak(std::move(weak))
|
||||
, _maxDuration(kMaxDuration)
|
||||
|
@ -136,14 +198,6 @@ RoundVideoRecorder::Private::~Private() {
|
|||
finishEncoding();
|
||||
}
|
||||
|
||||
int RoundVideoRecorder::Private::Write(void *opaque, uint8_t *buf, int buf_size) {
|
||||
return static_cast<Private*>(opaque)->write(buf, buf_size);
|
||||
}
|
||||
|
||||
int64_t RoundVideoRecorder::Private::Seek(void *opaque, int64_t offset, int whence) {
|
||||
return static_cast<Private*>(opaque)->seek(offset, whence);
|
||||
}
|
||||
|
||||
int RoundVideoRecorder::Private::write(uint8_t *buf, int buf_size) {
|
||||
if (const auto total = _resultOffset + int64(buf_size)) {
|
||||
const auto size = int64(_result.size());
|
||||
|
@ -376,7 +430,11 @@ void RoundVideoRecorder::Private::finishEncoding() {
|
|||
if (_format
|
||||
&& writeFrame(nullptr, _videoCodec, _videoStream)
|
||||
&& writeFrame(nullptr, _audioCodec, _audioStream)) {
|
||||
av_write_trailer(_format.get());
|
||||
const auto error = AvErrorWrap(av_write_trailer(_format.get()));
|
||||
if (error) {
|
||||
LogError("av_write_trailer", error);
|
||||
fail(Error::Encoding);
|
||||
}
|
||||
}
|
||||
deinitEncoding();
|
||||
}
|
||||
|
@ -391,19 +449,153 @@ RoundVideoResult RoundVideoRecorder::Private::finish() {
|
|||
return {};
|
||||
}
|
||||
finishEncoding();
|
||||
auto result = RoundVideoResult{
|
||||
auto result = appendToPrevious({
|
||||
.content = base::take(_result),
|
||||
.waveform = QByteArray(),
|
||||
.duration = base::take(_resultDuration),
|
||||
};
|
||||
});
|
||||
if (result.duration < kMinDuration) {
|
||||
return {};
|
||||
}
|
||||
_previousPartsDuration += result.duration;
|
||||
_maxDuration -= result.duration;
|
||||
return result;
|
||||
}
|
||||
|
||||
RoundVideoResult RoundVideoRecorder::Private::appendToPrevious(
|
||||
RoundVideoResult video) {
|
||||
if (!_previous.duration) {
|
||||
return video;
|
||||
}
|
||||
const auto cleanup = gsl::finally([&] {
|
||||
_forConcat1 = {};
|
||||
_forConcat2 = {};
|
||||
deinitEncoding();
|
||||
});
|
||||
|
||||
auto input1 = OpenInputContext(&_previous.content, &_forConcat1);
|
||||
auto input2 = OpenInputContext(&video.content, &_forConcat2);
|
||||
if (!input1 || !input2) {
|
||||
return video;
|
||||
}
|
||||
|
||||
auto output = MakeWriteFormatPointer(
|
||||
static_cast<void*>(this),
|
||||
nullptr,
|
||||
&Private::Write,
|
||||
&Private::Seek,
|
||||
"mp4"_q);
|
||||
|
||||
for (auto i = 0; i != input1->nb_streams; ++i) {
|
||||
AVStream *inStream = input1->streams[i];
|
||||
AVStream *outStream = avformat_new_stream(output.get(), nullptr);
|
||||
if (!outStream) {
|
||||
LogError("avformat_new_stream");
|
||||
fail(Error::Encoding);
|
||||
return {};
|
||||
}
|
||||
const auto error = AvErrorWrap(avcodec_parameters_copy(
|
||||
outStream->codecpar,
|
||||
inStream->codecpar));
|
||||
if (error) {
|
||||
LogError("avcodec_parameters_copy", error);
|
||||
fail(Error::Encoding);
|
||||
return {};
|
||||
}
|
||||
outStream->time_base = inStream->time_base;
|
||||
}
|
||||
|
||||
const auto offset = _previous.duration;
|
||||
auto context = CopyContext();
|
||||
auto error = AvErrorWrap(avformat_write_header(
|
||||
output.get(),
|
||||
nullptr));
|
||||
if (error) {
|
||||
LogError("avformat_write_header", error);
|
||||
fail(Error::Encoding);
|
||||
return {};
|
||||
} else if (!copyPackets(input1.get(), output.get(), context)
|
||||
|| !copyPackets(input2.get(), output.get(), context, offset)) {
|
||||
return {};
|
||||
}
|
||||
error = AvErrorWrap(av_write_trailer(output.get()));
|
||||
if (error) {
|
||||
LogError("av_write_trailer", error);
|
||||
fail(Error::Encoding);
|
||||
return {};
|
||||
}
|
||||
return RoundVideoResult{
|
||||
.content = base::take(_result),
|
||||
.waveform = QByteArray(),
|
||||
.duration = _previous.duration + video.duration,
|
||||
};
|
||||
}
|
||||
|
||||
FormatPointer RoundVideoRecorder::Private::OpenInputContext(
|
||||
not_null<const QByteArray*> data,
|
||||
not_null<ReadBytesWrap*> wrap) {
|
||||
*wrap = ReadBytesWrap{
|
||||
.size = data->size(),
|
||||
.data = reinterpret_cast<const uchar*>(data->constData()),
|
||||
};
|
||||
return MakeFormatPointer(
|
||||
wrap.get(),
|
||||
&ReadBytesWrap::Read,
|
||||
nullptr,
|
||||
&ReadBytesWrap::Seek);
|
||||
}
|
||||
|
||||
bool RoundVideoRecorder::Private::copyPackets(
|
||||
not_null<AVFormatContext*> input,
|
||||
not_null<AVFormatContext*> output,
|
||||
CopyContext &context,
|
||||
crl::time offset) {
|
||||
AVPacket packet;
|
||||
av_init_packet(&packet);
|
||||
|
||||
auto offsets = std::array<int64, kMaxStreams>{ 0 };
|
||||
while (av_read_frame(input, &packet) >= 0) {
|
||||
const auto index = packet.stream_index;
|
||||
Assert(index >= 0 && index < kMaxStreams);
|
||||
Assert(index < output->nb_streams);
|
||||
|
||||
if (offset) {
|
||||
auto &scaled = offsets[index];
|
||||
if (!scaled) {
|
||||
scaled = av_rescale_q(
|
||||
offset,
|
||||
AVRational{ 1, 1000 },
|
||||
input->streams[index]->time_base);
|
||||
}
|
||||
if (packet.pts != AV_NOPTS_VALUE) {
|
||||
packet.pts += scaled;
|
||||
}
|
||||
if (packet.dts != AV_NOPTS_VALUE) {
|
||||
packet.dts += scaled;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.pts <= context.lastPts[index]) {
|
||||
packet.pts = context.lastPts[index] + 1;
|
||||
}
|
||||
context.lastPts[index] = packet.pts;
|
||||
|
||||
if (packet.dts <= context.lastDts[index]) {
|
||||
packet.dts = context.lastDts[index] + 1;
|
||||
}
|
||||
context.lastDts[index] = packet.dts;
|
||||
|
||||
const auto error = AvErrorWrap(av_interleaved_write_frame(
|
||||
output,
|
||||
&packet));
|
||||
if (error) {
|
||||
LogError("av_interleaved_write_frame", error);
|
||||
av_packet_unref(&packet);
|
||||
return false;
|
||||
}
|
||||
av_packet_unref(&packet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoundVideoRecorder::Private::restart(RoundVideoPartial partial) {
|
||||
if (_format) {
|
||||
return;
|
||||
|
@ -411,7 +603,8 @@ void RoundVideoRecorder::Private::restart(RoundVideoPartial partial) {
|
|||
notifyFinished();
|
||||
return;
|
||||
}
|
||||
_previousContent = std::move(partial.content);
|
||||
_previous = std::move(partial.video);
|
||||
_maxDuration = kMaxDuration - _previous.duration;
|
||||
_finished = false;
|
||||
initEncoding();
|
||||
_timeoutTimer.callOnce(kInitTimeout);
|
||||
|
@ -664,7 +857,7 @@ void RoundVideoRecorder::Private::encodeAudioFrame(
|
|||
void RoundVideoRecorder::Private::notifyFinished() {
|
||||
_finished = true;
|
||||
_updates.fire({
|
||||
.samples = int((_previousPartsDuration + _resultDuration) * 48),
|
||||
.samples = int((_previous.duration + _resultDuration) * 48),
|
||||
.level = base::take(_maxLevelSinceLastUpdate),
|
||||
.finished = true,
|
||||
});
|
||||
|
@ -741,7 +934,7 @@ void RoundVideoRecorder::Private::updateResultDuration(
|
|||
if (initial || (_lastUpdateDuration + kUpdateEach < _resultDuration)) {
|
||||
_lastUpdateDuration = _resultDuration;
|
||||
_updates.fire({
|
||||
.samples = int((_previousPartsDuration + _resultDuration) * 48),
|
||||
.samples = int((_previous.duration + _resultDuration) * 48),
|
||||
.level = base::take(_maxLevelSinceLastUpdate),
|
||||
});
|
||||
}
|
||||
|
@ -986,3 +1179,4 @@ void RoundVideoRecorder::resume(RoundVideoPartial partial) {
|
|||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ struct RoundVideoResult {
|
|||
};
|
||||
|
||||
struct RoundVideoPartial {
|
||||
QByteArray content;
|
||||
RoundVideoResult video;
|
||||
crl::time from = 0;
|
||||
crl::time till = 0;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue