2019-02-13 18:10:18 +00:00
|
|
|
/*
|
|
|
|
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/streaming/media_streaming_file.h"
|
|
|
|
|
|
|
|
#include "media/streaming/media_streaming_loader.h"
|
2019-02-17 06:54:57 +00:00
|
|
|
#include "media/streaming/media_streaming_file_delegate.h"
|
2019-02-13 18:10:18 +00:00
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
#include "ui/toast/toast.h" // #TODO streaming
|
2019-02-13 18:10:18 +00:00
|
|
|
|
|
|
|
namespace Media {
|
|
|
|
namespace Streaming {
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
File::Context::Context(
|
|
|
|
not_null<FileDelegate*> delegate,
|
|
|
|
not_null<Reader*> reader)
|
|
|
|
: _delegate(delegate)
|
|
|
|
, _reader(reader)
|
|
|
|
, _size(reader->size()) {
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int File::Context::Read(void *opaque, uint8_t *buffer, int bufferSize) {
|
|
|
|
return static_cast<Context*>(opaque)->read(
|
|
|
|
bytes::make_span(buffer, bufferSize));
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t File::Context::Seek(void *opaque, int64_t offset, int whence) {
|
|
|
|
return static_cast<Context*>(opaque)->seek(offset, whence);
|
|
|
|
}
|
|
|
|
|
|
|
|
int File::Context::read(bytes::span buffer) {
|
|
|
|
const auto amount = std::min(size_type(_size - _offset), buffer.size());
|
|
|
|
if (unroll() || amount < 0) {
|
|
|
|
return -1;
|
|
|
|
} else if (!amount) {
|
|
|
|
return amount;
|
|
|
|
}
|
|
|
|
buffer = buffer.subspan(0, amount);
|
|
|
|
while (!_reader->fill(buffer, _offset, &_semaphore)) {
|
2019-02-21 13:40:09 +00:00
|
|
|
_delegate->fileWaitingForData();
|
2019-02-13 18:10:18 +00:00
|
|
|
_semaphore.acquire();
|
|
|
|
if (_interrupted) {
|
|
|
|
return -1;
|
|
|
|
} else if (_reader->failed()) {
|
2019-02-17 06:54:57 +00:00
|
|
|
fail();
|
2019-02-13 18:10:18 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_offset += amount;
|
|
|
|
return amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t File::Context::seek(int64_t offset, int whence) {
|
|
|
|
const auto checkedSeek = [&](int64_t offset) {
|
|
|
|
if (_failed || offset < 0 || offset > _size) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return (_offset = offset);
|
|
|
|
};
|
|
|
|
switch (whence) {
|
|
|
|
case SEEK_SET: return checkedSeek(offset);
|
|
|
|
case SEEK_CUR: return checkedSeek(_offset + offset);
|
|
|
|
case SEEK_END: return checkedSeek(_size + offset);
|
|
|
|
case AVSEEK_SIZE: return _size;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
void File::Context::logError(QLatin1String method) {
|
|
|
|
if (!unroll()) {
|
|
|
|
LogError(method);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void File::Context::logError(QLatin1String method, AvErrorWrap error) {
|
|
|
|
if (!unroll()) {
|
|
|
|
LogError(method, error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void File::Context::logFatal(QLatin1String method) {
|
|
|
|
if (!unroll()) {
|
|
|
|
LogError(method);
|
2019-02-17 06:54:57 +00:00
|
|
|
fail();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void File::Context::logFatal(QLatin1String method, AvErrorWrap error) {
|
|
|
|
if (!unroll()) {
|
|
|
|
LogError(method, error);
|
2019-02-17 06:54:57 +00:00
|
|
|
fail();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
Stream File::Context::initStream(AVMediaType type) {
|
|
|
|
auto result = Stream();
|
|
|
|
const auto index = result.index = av_find_best_stream(
|
2019-02-13 18:10:18 +00:00
|
|
|
_formatContext,
|
|
|
|
type,
|
|
|
|
-1,
|
|
|
|
-1,
|
2019-02-16 10:29:55 +00:00
|
|
|
nullptr,
|
2019-02-13 18:10:18 +00:00
|
|
|
0);
|
2019-02-17 06:54:57 +00:00
|
|
|
if (index < 0) {
|
|
|
|
return {};
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
const auto info = _formatContext->streams[index];
|
2019-02-22 11:58:26 +00:00
|
|
|
result.codec = MakeCodecPointer(info);
|
|
|
|
if (!result.codec) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2019-02-13 18:10:18 +00:00
|
|
|
if (type == AVMEDIA_TYPE_VIDEO) {
|
2019-02-22 11:58:26 +00:00
|
|
|
const auto codec = result.codec.get();
|
2019-02-17 06:54:57 +00:00
|
|
|
result.rotation = ReadRotationFromMetadata(info);
|
2019-02-22 11:58:26 +00:00
|
|
|
result.dimensions = QSize(codec->width, codec->height);
|
2019-02-17 06:54:57 +00:00
|
|
|
} else if (type == AVMEDIA_TYPE_AUDIO) {
|
|
|
|
result.frequency = info->codecpar->sample_rate;
|
|
|
|
if (!result.frequency) {
|
|
|
|
return {};
|
|
|
|
}
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
result.frame = MakeFramePointer();
|
|
|
|
if (!result.frame) {
|
|
|
|
return {};
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
2019-02-17 06:54:57 +00:00
|
|
|
result.timeBase = info->time_base;
|
|
|
|
result.duration = (info->duration != AV_NOPTS_VALUE)
|
2019-02-20 13:28:48 +00:00
|
|
|
? PtsToTimeCeil(info->duration, result.timeBase)
|
|
|
|
: PtsToTimeCeil(_formatContext->duration, kUniversalTimeBase);
|
|
|
|
if (result.duration == kTimeUnknown || !result.duration) {
|
|
|
|
return {};
|
|
|
|
}
|
2019-02-17 06:54:57 +00:00
|
|
|
return result;
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
2019-02-22 11:58:26 +00:00
|
|
|
void File::Context::seekToPosition(
|
|
|
|
const Stream &stream,
|
|
|
|
crl::time position) {
|
2019-02-13 18:10:18 +00:00
|
|
|
auto error = AvErrorWrap();
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
if (!position) {
|
2019-02-13 18:10:18 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-02-22 11:58:26 +00:00
|
|
|
//
|
|
|
|
// Non backward search reads the whole file if the position is after
|
|
|
|
// the last keyframe inside the index. So we search only backward.
|
|
|
|
//
|
|
|
|
//const auto seekFlags = 0;
|
|
|
|
//error = av_seek_frame(
|
|
|
|
// _formatContext,
|
|
|
|
// streamIndex,
|
|
|
|
// TimeToPts(position, kUniversalTimeBase),
|
|
|
|
// seekFlags);
|
|
|
|
//if (!error) {
|
|
|
|
// return;
|
|
|
|
//}
|
|
|
|
//
|
2019-02-13 18:10:18 +00:00
|
|
|
error = av_seek_frame(
|
|
|
|
_formatContext,
|
2019-02-22 11:58:26 +00:00
|
|
|
stream.index,
|
|
|
|
TimeToPts(
|
|
|
|
std::clamp(position, crl::time(0), stream.duration - 1),
|
|
|
|
stream.timeBase),
|
|
|
|
AVSEEK_FLAG_BACKWARD);
|
2019-02-13 18:10:18 +00:00
|
|
|
if (!error) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return logFatal(qstr("av_seek_frame"), error);
|
|
|
|
}
|
|
|
|
|
|
|
|
base::variant<Packet, AvErrorWrap> File::Context::readPacket() {
|
|
|
|
auto error = AvErrorWrap();
|
|
|
|
|
|
|
|
auto result = Packet();
|
|
|
|
error = av_read_frame(_formatContext, &result.fields());
|
|
|
|
if (unroll()) {
|
|
|
|
return AvErrorWrap();
|
|
|
|
} else if (!error) {
|
|
|
|
return std::move(result);
|
|
|
|
} else if (error.code() != AVERROR_EOF) {
|
|
|
|
logFatal(qstr("av_read_frame"), error);
|
|
|
|
}
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
void File::Context::start(crl::time position) {
|
2019-02-13 18:10:18 +00:00
|
|
|
auto error = AvErrorWrap();
|
|
|
|
|
|
|
|
if (unroll()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_ioBuffer = reinterpret_cast<uchar*>(av_malloc(AVBlockSize));
|
|
|
|
_ioContext = avio_alloc_context(
|
|
|
|
_ioBuffer,
|
|
|
|
AVBlockSize,
|
|
|
|
0,
|
|
|
|
static_cast<void*>(this),
|
|
|
|
&Context::Read,
|
|
|
|
nullptr,
|
|
|
|
&Context::Seek);
|
|
|
|
_formatContext = avformat_alloc_context();
|
|
|
|
if (!_formatContext) {
|
|
|
|
return logFatal(qstr("avformat_alloc_context"));
|
|
|
|
}
|
|
|
|
_formatContext->pb = _ioContext;
|
|
|
|
|
2019-02-22 11:58:26 +00:00
|
|
|
auto options = (AVDictionary*)nullptr;
|
|
|
|
const auto guard = gsl::finally([&] { av_dict_free(&options); });
|
|
|
|
av_dict_set(&options, "usetoc", "1", 0);
|
|
|
|
error = avformat_open_input(
|
|
|
|
&_formatContext,
|
|
|
|
nullptr,
|
|
|
|
nullptr,
|
|
|
|
&options);
|
2019-02-13 18:10:18 +00:00
|
|
|
if (error) {
|
|
|
|
_ioBuffer = nullptr;
|
|
|
|
return logFatal(qstr("avformat_open_input"), error);
|
|
|
|
}
|
|
|
|
_opened = true;
|
2019-02-22 11:58:26 +00:00
|
|
|
_formatContext->flags |= AVFMT_FLAG_FAST_SEEK;
|
2019-02-13 18:10:18 +00:00
|
|
|
|
|
|
|
if ((error = avformat_find_stream_info(_formatContext, nullptr))) {
|
|
|
|
return logFatal(qstr("avformat_find_stream_info"), error);
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
auto video = initStream(AVMEDIA_TYPE_VIDEO);
|
2019-02-13 18:10:18 +00:00
|
|
|
if (unroll()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
auto audio = initStream(AVMEDIA_TYPE_AUDIO);
|
2019-02-13 18:10:18 +00:00
|
|
|
if (unroll()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-22 11:58:26 +00:00
|
|
|
if (video.codec || audio.codec) {
|
|
|
|
seekToPosition(video.codec ? video : audio, position);
|
|
|
|
}
|
2019-02-13 18:10:18 +00:00
|
|
|
if (unroll()) {
|
2019-02-17 06:54:57 +00:00
|
|
|
return;
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
_delegate->fileReady(std::move(video), std::move(audio));
|
|
|
|
}
|
|
|
|
|
2019-02-13 18:10:18 +00:00
|
|
|
void File::Context::readNextPacket() {
|
|
|
|
auto result = readPacket();
|
|
|
|
if (unroll()) {
|
|
|
|
return;
|
2019-02-17 06:54:57 +00:00
|
|
|
} else if (const auto packet = base::get_if<Packet>(&result)) {
|
|
|
|
const auto more = _delegate->fileProcessPacket(std::move(*packet));
|
2019-02-20 13:28:48 +00:00
|
|
|
if (!more) {
|
|
|
|
do {
|
|
|
|
_semaphore.acquire();
|
|
|
|
} while (!unroll() && !_delegate->fileReadMore());
|
|
|
|
}
|
2019-02-13 18:10:18 +00:00
|
|
|
} else {
|
|
|
|
// Still trying to read by drain.
|
|
|
|
Assert(result.is<AvErrorWrap>());
|
|
|
|
Assert(result.get<AvErrorWrap>().code() == AVERROR_EOF);
|
2019-02-17 06:54:57 +00:00
|
|
|
handleEndOfFile();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
}
|
2019-02-17 06:54:57 +00:00
|
|
|
void File::Context::handleEndOfFile() {
|
|
|
|
const auto more = _delegate->fileProcessPacket(Packet());
|
2019-02-20 13:28:48 +00:00
|
|
|
// #TODO streaming looping
|
2019-02-17 06:54:57 +00:00
|
|
|
_readTillEnd = true;
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void File::Context::interrupt() {
|
|
|
|
_interrupted = true;
|
|
|
|
_semaphore.release();
|
|
|
|
}
|
|
|
|
|
2019-02-20 13:28:48 +00:00
|
|
|
void File::Context::wake() {
|
|
|
|
_semaphore.release();
|
|
|
|
}
|
|
|
|
|
2019-02-13 18:10:18 +00:00
|
|
|
bool File::Context::interrupted() const {
|
|
|
|
return _interrupted;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool File::Context::failed() const {
|
|
|
|
return _failed;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool File::Context::unroll() const {
|
|
|
|
return failed() || interrupted();
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
void File::Context::fail() {
|
|
|
|
_failed = true;
|
|
|
|
_delegate->fileError();
|
|
|
|
}
|
2019-02-13 18:10:18 +00:00
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
File::Context::~Context() {
|
2019-02-13 18:10:18 +00:00
|
|
|
if (_opened) {
|
|
|
|
avformat_close_input(&_formatContext);
|
|
|
|
}
|
|
|
|
if (_ioContext) {
|
|
|
|
av_freep(&_ioContext->buffer);
|
|
|
|
av_freep(&_ioContext);
|
|
|
|
} else if (_ioBuffer) {
|
|
|
|
av_freep(&_ioBuffer);
|
|
|
|
}
|
|
|
|
if (_formatContext) {
|
|
|
|
avformat_free_context(_formatContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool File::Context::finished() const {
|
2019-02-17 06:54:57 +00:00
|
|
|
return unroll() || _readTillEnd; // #TODO streaming looping
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
File::File(
|
|
|
|
not_null<Data::Session*> owner,
|
|
|
|
std::unique_ptr<Loader> loader)
|
|
|
|
: _reader(owner, std::move(loader)) {
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
void File::start(not_null<FileDelegate*> delegate, crl::time position) {
|
|
|
|
stop();
|
|
|
|
|
|
|
|
_context.emplace(delegate, &_reader);
|
2019-02-17 11:08:29 +00:00
|
|
|
_thread = std::thread([=, context = &*_context] {
|
2019-02-17 06:54:57 +00:00
|
|
|
context->start(position);
|
|
|
|
while (!context->finished()) {
|
|
|
|
context->readNextPacket();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
2019-02-17 06:54:57 +00:00
|
|
|
|
|
|
|
crl::on_main(context, [] { AssertIsDebug();
|
|
|
|
Ui::Toast::Show("Finished loading.");
|
|
|
|
});
|
2019-02-13 18:10:18 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-02-20 13:28:48 +00:00
|
|
|
void File::wake() {
|
|
|
|
Expects(_context.has_value());
|
|
|
|
|
|
|
|
_context->wake();
|
|
|
|
}
|
|
|
|
|
2019-02-17 06:54:57 +00:00
|
|
|
void File::stop() {
|
2019-02-13 18:10:18 +00:00
|
|
|
if (_thread.joinable()) {
|
|
|
|
_context->interrupt();
|
|
|
|
_thread.join();
|
|
|
|
}
|
2019-02-17 06:54:57 +00:00
|
|
|
_context.reset();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
File::~File() {
|
2019-02-17 06:54:57 +00:00
|
|
|
stop();
|
2019-02-13 18:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Streaming
|
|
|
|
} // namespace Media
|