Move some logic to Media::Streaming::Player.

This commit is contained in:
John Preston 2019-02-17 10:54:57 +04:00
parent 64f2f330f6
commit a093cb6274
12 changed files with 633 additions and 493 deletions

View file

@ -29,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h" #include "core/application.h"
// #TODO streaming // #TODO streaming
#include "media/streaming/media_streaming_file.h" #include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_loader_mtproto.h" #include "media/streaming/media_streaming_loader_mtproto.h"
namespace { namespace {
@ -307,18 +307,18 @@ void DocumentOpenClickHandler::Open(
return; return;
} }
} }
if (playMusic || playVideo) { if (data->isAudioFile() || data->isVideoFile()) {
AssertIsDebug(); AssertIsDebug();
if (auto loader = data->createStreamingLoader(origin)) { if (auto loader = data->createStreamingLoader(origin)) {
static auto file = std::unique_ptr<Media::Streaming::File>(); static auto player = std::unique_ptr<Media::Streaming::Player>();
file = std::make_unique<Media::Streaming::File>( player = std::make_unique<Media::Streaming::Player>(
&data->owner(), &data->owner(),
std::move(loader)); std::move(loader));
data->session().lifetime().add([] { data->session().lifetime().add([] {
file = nullptr; player = nullptr;
}); });
file->start( player->init(
(playMusic (data->isAudioFile()
? Media::Streaming::Mode::Audio ? Media::Streaming::Mode::Audio
: Media::Streaming::Mode::Video), : Media::Streaming::Mode::Video),
0); 0);
@ -431,6 +431,25 @@ void DocumentSaveClickHandler::Save(
bool forceSavingAs) { bool forceSavingAs) {
if (!data->date) return; if (!data->date) return;
if (data->isAudioFile() || data->isVideoFile()) {
AssertIsDebug();
if (auto loader = data->createStreamingLoader(origin)) {
static auto player = std::unique_ptr<Media::Streaming::Player>();
player = std::make_unique<Media::Streaming::Player>(
&data->owner(),
std::move(loader));
data->session().lifetime().add([] {
player = nullptr;
});
player->init(
(data->isAudioFile()
? Media::Streaming::Mode::Audio
: Media::Streaming::Mode::Video),
0);
}
return;
}
auto filepath = data->filepath( auto filepath = data->filepath(
DocumentData::FilePathResolveSaveFromDataSilent, DocumentData::FilePathResolveSaveFromDataSilent,
forceSavingAs); forceSavingAs);

View file

@ -302,13 +302,18 @@ void MainWindow::destroyLayer() {
if (!_layer) { if (!_layer) {
return; return;
} }
const auto resetFocus = Ui::InFocusChain(_layer); auto layer = base::take(_layer);
if (resetFocus) setFocus(); const auto resetFocus = Ui::InFocusChain(layer);
_layer = nullptr; if (resetFocus) {
setFocus();
}
layer = nullptr;
if (controller()) { if (controller()) {
controller()->disableGifPauseReason(Window::GifPauseReason::Layer); controller()->disableGifPauseReason(Window::GifPauseReason::Layer);
} }
if (resetFocus) setInnerFocus(); if (resetFocus) {
setInnerFocus();
}
InvokeQueued(this, [=] { InvokeQueued(this, [=] {
checkHistoryActivation(); checkHistoryActivation();
}); });

View file

@ -7,158 +7,49 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
} // extern "C"
namespace Media { namespace Media {
namespace Streaming { namespace Streaming {
constexpr auto kTimeUnknown = crl::time(-1);
enum class Mode { enum class Mode {
Both, Both,
Audio, Audio,
Video, Video,
Inspection Inspection,
}; };
struct Information { struct Information {
static constexpr auto kDurationUnknown = crl::time(-1); crl::time videoStarted = kTimeUnknown;
crl::time videoDuration = kTimeUnknown;
QSize videoSize;
QImage videoCover;
int videoCoverRotation = 0;
QSize video; crl::time audioStarted = kTimeUnknown;
bool audio = false; crl::time audioDuration = kTimeUnknown;
crl::time duration = kDurationUnknown;
crl::time started = 0;
QImage cover;
int rotation = 0;
}; };
class AvErrorWrap { struct RepaintRequest {
public: crl::time position;
AvErrorWrap(int code = 0) : _code(code) {
}
[[nodiscard]] explicit operator bool() const {
return (_code < 0);
}
[[nodiscard]] int code() const {
return _code;
}
[[nodiscard]] QString text() const {
char string[AV_ERROR_MAX_STRING_SIZE] = { 0 };
return QString::fromUtf8(av_make_error_string(
string,
sizeof(string),
_code));
}
private:
int _code = 0;
}; };
class Packet { struct WaitingForData {
public:
Packet() {
setEmpty();
}
Packet(const AVPacket &data) {
bytes::copy(_data, bytes::object_as_span(&data));
}
Packet(Packet &&other) {
bytes::copy(_data, other._data);
if (!other.empty()) {
other.release();
}
}
Packet &operator=(Packet &&other) {
if (this != &other) {
av_packet_unref(&fields());
bytes::copy(_data, other._data);
if (!other.empty()) {
other.release();
}
}
return *this;
}
~Packet() {
av_packet_unref(&fields());
}
[[nodiscard]] AVPacket &fields() {
return *reinterpret_cast<AVPacket*>(_data);
}
[[nodiscard]] const AVPacket &fields() const {
return *reinterpret_cast<const AVPacket*>(_data);
}
[[nodiscard]] bool empty() const {
return !fields().data;
}
void release() {
setEmpty();
}
private:
void setEmpty() {
auto &native = fields();
av_init_packet(&native);
native.data = nullptr;
native.size = 0;
}
alignas(alignof(AVPacket)) bytes::type _data[sizeof(AVPacket)];
}; };
struct CodecDeleter { struct MutedByOther {
void operator()(AVCodecContext *value);
};
using CodecPointer = std::unique_ptr<AVCodecContext, CodecDeleter>;
CodecPointer MakeCodecPointer(not_null<AVStream*> stream);
struct FrameDeleter {
void operator()(AVFrame *value);
};
using FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;
FramePointer MakeFramePointer();
struct SwsContextDeleter {
void operator()(SwsContext *value);
};
using SwsContextPointer = std::unique_ptr<SwsContext, SwsContextDeleter>;
SwsContextPointer MakeSwsContextPointer(
not_null<AVFrame*> frame,
QSize resize,
SwsContextPointer *existing = nullptr);
struct Stream {
CodecPointer codec;
FramePointer frame;
std::deque<Packet> queue;
crl::time lastReadPositionTime = 0;
int invalidDataPackets = 0;
// Video only.
int rotation = 0;
SwsContextPointer swsContext;
}; };
void LogError(QLatin1String method); struct Update {
void LogError(QLatin1String method, AvErrorWrap error); base::variant<
Information,
RepaintRequest,
WaitingForData,
MutedByOther> data;
};
[[nodiscard]] crl::time PtsToTime(int64_t pts, const AVRational &timeBase); struct Error {
[[nodiscard]] int ReadRotationFromMetadata(not_null<AVStream*> stream); };
[[nodiscard]] bool RotationSwapWidthHeight(int rotation);
[[nodiscard]] std::optional<AvErrorWrap> ReadNextFrame(Stream &stream);
[[nodiscard]] QImage ConvertFrame(
Stream& stream,
QSize resize,
QImage storage);
} // namespace Streaming } // namespace Streaming
} // namespace Media } // namespace Media

View file

@ -8,18 +8,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_file.h" #include "media/streaming/media_streaming_file.h"
#include "media/streaming/media_streaming_loader.h" #include "media/streaming/media_streaming_loader.h"
#include "media/streaming/media_streaming_file_delegate.h"
#include "media/audio/media_audio.h" // #TODO streaming #include "ui/toast/toast.h" // #TODO streaming
#include "media/audio/media_child_ffmpeg_loader.h"
#include "ui/toast/toast.h"
namespace Media { namespace Media {
namespace Streaming { namespace Streaming {
File::Context::Context(not_null<Reader*> reader) File::Context::Context(
: _reader(reader) not_null<FileDelegate*> delegate,
, _size(reader->size()) not_null<Reader*> reader)
, _audioMsgId(AudioMsgId::ForVideo()) { : _delegate(delegate)
, _reader(reader)
, _size(reader->size()) {
} }
int File::Context::Read(void *opaque, uint8_t *buffer, int bufferSize) { int File::Context::Read(void *opaque, uint8_t *buffer, int bufferSize) {
@ -44,7 +45,7 @@ int File::Context::read(bytes::span buffer) {
if (_interrupted) { if (_interrupted) {
return -1; return -1;
} else if (_reader->failed()) { } else if (_reader->failed()) {
_failed = true; fail();
return -1; return -1;
} }
} }
@ -83,70 +84,68 @@ void File::Context::logError(QLatin1String method, AvErrorWrap error) {
void File::Context::logFatal(QLatin1String method) { void File::Context::logFatal(QLatin1String method) {
if (!unroll()) { if (!unroll()) {
LogError(method); LogError(method);
_failed = true; fail();
} }
} }
void File::Context::logFatal(QLatin1String method, AvErrorWrap error) { void File::Context::logFatal(QLatin1String method, AvErrorWrap error) {
if (!unroll()) { if (!unroll()) {
LogError(method, error); LogError(method, error);
_failed = true; fail();
} }
} }
void File::Context::initStream(StreamWrap &wrap, AVMediaType type) { Stream File::Context::initStream(AVMediaType type) {
wrap.id = av_find_best_stream( auto result = Stream();
const auto index = result.index = av_find_best_stream(
_formatContext, _formatContext,
type, type,
-1, -1,
-1, -1,
nullptr, nullptr,
0); 0);
if (wrap.id < 0) { if (index < 0) {
return; return {};
} }
wrap.info = _formatContext->streams[wrap.id]; const auto info = _formatContext->streams[index];
if (type == AVMEDIA_TYPE_VIDEO) { if (type == AVMEDIA_TYPE_VIDEO) {
wrap.stream.rotation = ReadRotationFromMetadata(wrap.info); result.rotation = ReadRotationFromMetadata(info);
} else if (type == AVMEDIA_TYPE_AUDIO) {
result.frequency = info->codecpar->sample_rate;
if (!result.frequency) {
return {};
}
} }
wrap.stream.codec = MakeCodecPointer(wrap.info); result.codec = MakeCodecPointer(info);
if (!wrap.stream.codec) { if (!result.codec) {
ClearStream(wrap); return {};
return;
} }
wrap.stream.frame = MakeFramePointer(); result.frame = MakeFramePointer();
if (!wrap.stream.frame) { if (!result.frame) {
ClearStream(wrap); return {};
return;
} }
result.timeBase = info->time_base;
result.duration = (info->duration != AV_NOPTS_VALUE)
? PtsToTime(info->duration, result.timeBase)
: PtsToTime(_formatContext->duration, kUniversalTimeBase);
return result;
} }
void File::Context::seekToPosition(crl::time positionTime) { void File::Context::seekToPosition(crl::time position) {
auto error = AvErrorWrap(); auto error = AvErrorWrap();
if (!positionTime) { if (!position) {
return; return;
} }
const auto &main = mainStream(); const auto streamIndex = -1;
Assert(main.info != nullptr); const auto seekFlags = 0;
const auto timeBase = main.info->time_base;
const auto timeStamp = (positionTime * timeBase.den)
/ (1000LL * timeBase.num);
error = av_seek_frame( error = av_seek_frame(
_formatContext, _formatContext,
main.id, streamIndex,
timeStamp, TimeToPts(position, kUniversalTimeBase),
0); seekFlags);
if (!error) {
return;
}
error = av_seek_frame(
_formatContext,
main.id,
timeStamp,
AVSEEK_FLAG_BACKWARD);
if (!error) { if (!error) {
return; return;
} }
@ -168,10 +167,9 @@ base::variant<Packet, AvErrorWrap> File::Context::readPacket() {
return error; return error;
} }
void File::Context::start(Mode mode, crl::time positionTime) { void File::Context::start(crl::time position) {
auto error = AvErrorWrap(); auto error = AvErrorWrap();
_mode = mode;
if (unroll()) { if (unroll()) {
return; return;
} }
@ -201,221 +199,131 @@ void File::Context::start(Mode mode, crl::time positionTime) {
return logFatal(qstr("avformat_find_stream_info"), error); return logFatal(qstr("avformat_find_stream_info"), error);
} }
initStream(_video, AVMEDIA_TYPE_VIDEO); auto video = initStream(AVMEDIA_TYPE_VIDEO);
initStream(_audio, AVMEDIA_TYPE_AUDIO);
if (!mainStreamUnchecked().info) {
return logFatal(qstr("RequiredStreamAbsent"));
}
readInformation(positionTime);
if (_audio.info
&& (_mode == Mode::Audio || _mode == Mode::Both)) { // #TODO streaming
Player::mixer()->resume(_audioMsgId, true);
}
}
auto File::Context::mainStreamUnchecked() const -> const StreamWrap & {
return (_mode == Mode::Video || (_video.info && _mode != Mode::Audio))
? _video
: _audio;
}
auto File::Context::mainStream() const -> const StreamWrap & {
const auto &result = mainStreamUnchecked();
Ensures(result.info != nullptr);
return result;
}
auto File::Context::mainStream() -> StreamWrap & {
return const_cast<StreamWrap&>(((const Context*)this)->mainStream());
}
void File::Context::readInformation(crl::time positionTime) {
const auto &main = mainStream();
const auto info = main.info;
auto information = Information();
information.duration = PtsToTime(info->duration, info->time_base);
auto result = readPacket();
const auto packet = base::get_if<Packet>(&result);
if (unroll()) { if (unroll()) {
return; return;
} else if (packet) {
if (positionTime > 0) {
const auto time = CountPacketPositionTime(
_formatContext->streams[packet->fields().stream_index],
*packet);
information.started = (time == Information::kDurationUnknown)
? positionTime
: time;
}
} else {
information.started = positionTime;
} }
if (_audio.info auto audio = initStream(AVMEDIA_TYPE_AUDIO);
&& (_mode == Mode::Audio || _mode == Mode::Both)) { // #TODO streaming
auto soundData = std::make_unique<VideoSoundData>();
soundData->context = _audio.stream.codec.release();
soundData->frequency = _audio.info->codecpar->sample_rate;
if (_audio.info->duration == AV_NOPTS_VALUE) {
soundData->length = (_formatContext->duration * soundData->frequency) / AV_TIME_BASE;
} else {
soundData->length = (_audio.info->duration * soundData->frequency * _audio.info->time_base.num) / _audio.info->time_base.den;
}
Player::mixer()->play(_audioMsgId, std::move(soundData), information.started);
}
if (packet) {
processPacket(std::move(*packet));
} else {
enqueueEofPackets();
}
information.cover = readFirstVideoFrame();
if (unroll()) { if (unroll()) {
return; return;
} else if (!information.cover.isNull()) {
information.video = information.cover.size();
information.rotation = _video.stream.rotation;
if (RotationSwapWidthHeight(information.rotation)) {
information.video.transpose();
}
} }
information.audio = (_audio.info != nullptr); seekToPosition(position);
_information = std::move(information);
}
QImage File::Context::readFirstVideoFrame() {
auto result = QImage();
while (_video.info && result.isNull()) {
auto frame = tryReadFirstVideoFrame();
if (unroll()) { if (unroll()) {
return QImage(); return;
} }
frame.match([&](QImage &image) {
if (!image.isNull()) { _delegate->fileReady(std::move(video), std::move(audio));
result = std::move(image);
} else {
ClearStream(_video);
}
}, [&](const AvErrorWrap &error) {
if (error.code() == AVERROR(EAGAIN)) {
readNextPacket();
} else {
ClearStream(_video);
}
});
}
if (!_video.info && _mode == Mode::Video) {
logFatal(qstr("RequiredStreamEmpty"));
return QImage();
}
return result;
} }
base::variant<QImage, AvErrorWrap> File::Context::tryReadFirstVideoFrame() { //void File::Context::readInformation(crl::time position) {
Expects(_video.info != nullptr); // auto information = Information();
// auto result = readPacket();
if (unroll()) { // const auto packet = base::get_if<Packet>(&result);
return AvErrorWrap(); // if (unroll()) {
} // return;
const auto error = ReadNextFrame(_video.stream); // } else if (packet) {
if (error) { // if (position > 0) {
if (error->code() == AVERROR_EOF) { // const auto time = CountPacketPosition(
// No valid video stream. // _formatContext->streams[packet->fields().stream_index],
if (_mode == Mode::Video) { // *packet);
logFatal(qstr("RequiredStreamEmpty")); // information.started = (time == Information::kDurationUnknown)
} // ? position
return QImage(); // : time;
} else if (error->code() != AVERROR(EAGAIN)) { // }
_failed = true; // } else {
} // information.started = position;
return *error; // }
} //
return ConvertFrame(_video.stream, QSize(), QImage()); // if (packet) {
} // processPacket(std::move(*packet));
// } else {
void File::Context::enqueueEofPackets() { // enqueueEofPackets();
if (_audio.info) { // }
Enqueue(_audio, Packet()); //
} // information.cover = readFirstVideoFrame();
if (_video.info) { // if (unroll()) {
Enqueue(_video, Packet()); // return;
} // } else if (!information.cover.isNull()) {
_readTillEnd = true; // information.video = information.cover.size();
} // information.rotation = _video.stream.rotation;
// if (RotationSwapWidthHeight(information.rotation)) {
void File::Context::processPacket(Packet &&packet) { // information.video.transpose();
const auto &native = packet.fields(); // }
const auto streamId = native.stream_index; // }
const auto check = [&](StreamWrap &wrap) { //
if ((native.stream_index == wrap.id) && wrap.info) { // information.audio = (_audio.info != nullptr);
// #TODO streaming queue packet to audio player // _information = std::move(information);
if ((_mode == Mode::Audio || _mode == Mode::Both) //}
&& (wrap.info == _audio.info)) { //
Player::mixer()->feedFromVideo({ &native, _audioMsgId }); //QImage File::Context::readFirstVideoFrame() {
packet.release(); // auto result = QImage();
} else { // while (_video.info && result.isNull()) {
Enqueue(wrap, std::move(packet)); // auto frame = tryReadFirstVideoFrame();
} // if (unroll()) {
return true; // return QImage();
} // }
return false; // frame.match([&](QImage &image) {
}; // if (!image.isNull()) {
// result = std::move(image);
check(_audio) || check(_video); // } else {
} // _video = StreamWrap();
// }
// }, [&](const AvErrorWrap &error) {
// if (error.code() == AVERROR(EAGAIN)) {
// readNextPacket();
// } else {
// _video = StreamWrap();
// }
// });
// }
// if (!_video.info && _mode == Mode::Video) {
// logFatal(qstr("RequiredStreamEmpty"));
// return QImage();
// }
// return result;
//}
//
//base::variant<QImage, AvErrorWrap> File::Context::tryReadFirstVideoFrame() {
// Expects(_video.info != nullptr);
//
// if (unroll()) {
// return AvErrorWrap();
// }
// const auto error = ReadNextFrame(_video.stream);
// if (error) {
// if (error->code() == AVERROR_EOF) {
// // No valid video stream.
// if (_mode == Mode::Video) {
// logFatal(qstr("RequiredStreamEmpty"));
// }
// return QImage();
// } else if (error->code() != AVERROR(EAGAIN)) {
// fail();
// }
// return *error;
// }
// return ConvertFrame(_video.stream, QSize(), QImage());
//}
void File::Context::readNextPacket() { void File::Context::readNextPacket() {
auto result = readPacket(); auto result = readPacket();
const auto packet = base::get_if<Packet>(&result);
if (unroll()) { if (unroll()) {
return; return;
} else if (packet) { } else if (const auto packet = base::get_if<Packet>(&result)) {
processPacket(std::move(*packet)); const auto more = _delegate->fileProcessPacket(std::move(*packet));
} else { } else {
// Still trying to read by drain. // Still trying to read by drain.
Assert(result.is<AvErrorWrap>()); Assert(result.is<AvErrorWrap>());
Assert(result.get<AvErrorWrap>().code() == AVERROR_EOF); Assert(result.get<AvErrorWrap>().code() == AVERROR_EOF);
enqueueEofPackets(); handleEndOfFile();
} }
} }
void File::Context::handleEndOfFile() {
crl::time File::Context::CountPacketPositionTime( // #TODO streaming looping
not_null<const AVStream*> info, const auto more = _delegate->fileProcessPacket(Packet());
const Packet &packet) { _readTillEnd = true;
const auto &native = packet.fields();
const auto packetPts = (native.pts == AV_NOPTS_VALUE)
? native.dts
: native.pts;
const auto &timeBase = info->time_base;
return PtsToTime(packetPts, info->time_base);
}
void File::Context::ClearStream(StreamWrap &wrap) {
wrap.id = -1;
wrap.stream = Stream();
wrap.info = nullptr;
}
crl::time File::Context::CountPacketPositionTime(
const StreamWrap &wrap,
const Packet &packet) {
return CountPacketPositionTime(wrap.info, packet);
}
void File::Context::Enqueue(StreamWrap &wrap, Packet &&packet) {
const auto time = CountPacketPositionTime(wrap, packet);
if (time != Information::kDurationUnknown) {
wrap.stream.lastReadPositionTime = time;
}
QMutexLocker lock(&wrap.mutex);
wrap.stream.queue.push_back(std::move(packet));
} }
void File::Context::interrupt() { void File::Context::interrupt() {
@ -435,10 +343,12 @@ bool File::Context::unroll() const {
return failed() || interrupted(); return failed() || interrupted();
} }
File::Context::~Context() { void File::Context::fail() {
ClearStream(_audio); _failed = true;
ClearStream(_video); _delegate->fileError();
}
File::Context::~Context() {
if (_opened) { if (_opened) {
avformat_close_input(&_formatContext); avformat_close_input(&_formatContext);
} }
@ -453,18 +363,8 @@ File::Context::~Context() {
} }
} }
bool File::Context::started() const {
return _information.has_value();
}
bool File::Context::finished() const { bool File::Context::finished() const {
return unroll() || _readTillEnd; return unroll() || _readTillEnd; // #TODO streaming looping
}
const Media::Streaming::Information & File::Context::information() const {
Expects(_information.has_value());
return *_information;
} }
File::File( File::File(
@ -473,54 +373,32 @@ File::File(
: _reader(owner, std::move(loader)) { : _reader(owner, std::move(loader)) {
} }
void File::start(Mode mode, crl::time positionTime) { void File::start(not_null<FileDelegate*> delegate, crl::time position) {
finish(); stop();
_context = std::make_unique<Context>(&_reader); _context.emplace(delegate, &_reader);
_thread = std::thread([=, context = _context.get()] { _thread = std::thread([=, context = &_context.value()] {
context->start(mode, positionTime); context->start(position);
if (context->interrupted()) {
return;
} else if (context->failed()) {
crl::on_main(context, [=] {
// #TODO streaming failed
});
} else {
crl::on_main(context, [=, info = context->information()] {
// #TODO streaming started
});
while (!context->finished()) { while (!context->finished()) {
context->readNextPacket(); context->readNextPacket();
} }
crl::on_main(context, [] { AssertIsDebug(); crl::on_main(context, [] { AssertIsDebug();
Ui::Toast::Show("Finished loading."); Ui::Toast::Show("Finished loading.");
}); });
}
}); });
} }
//rpl::producer<Information> File::information() const { void File::stop() {
//
//}
//
//rpl::producer<Packet> File::video() const {
//
//}
//
//rpl::producer<Packet> File::audio() const {
//
//}
void File::finish() {
if (_thread.joinable()) { if (_thread.joinable()) {
_context->interrupt(); _context->interrupt();
_thread.join(); _thread.join();
} }
_context = nullptr; _context.reset();
} }
File::~File() { File::~File() {
finish(); stop();
} }
} // namespace Streaming } // namespace Streaming

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "media/streaming/media_streaming_common.h" #include "media/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_utility.h"
#include "media/streaming/media_streaming_reader.h" #include "media/streaming/media_streaming_reader.h"
#include "base/bytes.h" #include "base/bytes.h"
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
@ -22,6 +23,7 @@ namespace Media {
namespace Streaming { namespace Streaming {
class Loader; class Loader;
class FileDelegate;
class File final { class File final {
public: public:
@ -30,41 +32,28 @@ public:
File(const File &other) = delete; File(const File &other) = delete;
File &operator=(const File &other) = delete; File &operator=(const File &other) = delete;
void start(Mode mode, crl::time positionTime); void start(not_null<FileDelegate*> delegate, crl::time position);
void wake();
//rpl::producer<Information> information() const; void stop();
//rpl::producer<Packet> video() const;
//rpl::producer<Packet> audio() const;
~File(); ~File();
private: private:
void finish();
class Context final : public base::has_weak_ptr { class Context final : public base::has_weak_ptr {
public: public:
Context(not_null<Reader*> reader); Context(not_null<FileDelegate*> delegate, not_null<Reader*> reader);
void start(Mode mode, crl::time positionTime); void start(crl::time position);
void readNextPacket(); void readNextPacket();
void interrupt(); void interrupt();
[[nodiscard]] bool interrupted() const; [[nodiscard]] bool interrupted() const;
[[nodiscard]] bool failed() const; [[nodiscard]] bool failed() const;
[[nodiscard]] bool started() const;
[[nodiscard]] bool finished() const; [[nodiscard]] bool finished() const;
[[nodiscard]] const Information &information() const;
~Context(); ~Context();
private: private:
struct StreamWrap {
int id = -1;
AVStream *info = nullptr;
Stream stream;
QMutex mutex;
};
static int Read(void *opaque, uint8_t *buffer, int bufferSize); static int Read(void *opaque, uint8_t *buffer, int bufferSize);
static int64_t Seek(void *opaque, int64_t offset, int whence); static int64_t Seek(void *opaque, int64_t offset, int whence);
@ -76,36 +65,19 @@ private:
void logError(QLatin1String method, AvErrorWrap error); void logError(QLatin1String method, AvErrorWrap error);
void logFatal(QLatin1String method); void logFatal(QLatin1String method);
void logFatal(QLatin1String method, AvErrorWrap error); void logFatal(QLatin1String method, AvErrorWrap error);
void fail();
void initStream(StreamWrap &wrap, AVMediaType type); Stream initStream(AVMediaType type);
void seekToPosition(crl::time positionTime); void seekToPosition(crl::time position);
// #TODO base::expected. // TODO base::expected.
[[nodiscard]] base::variant<Packet, AvErrorWrap> readPacket(); [[nodiscard]] base::variant<Packet, AvErrorWrap> readPacket();
void processPacket(Packet &&packet); void handleEndOfFile();
[[nodiscard]] const StreamWrap &mainStreamUnchecked() const;
[[nodiscard]] const StreamWrap &mainStream() const;
[[nodiscard]] StreamWrap &mainStream();
void readInformation(crl::time positionTime);
[[nodiscard]] QImage readFirstVideoFrame(); const not_null<FileDelegate*> _delegate;
[[nodiscard]] auto tryReadFirstVideoFrame() const not_null<Reader*> _reader;
-> base::variant<QImage, AvErrorWrap>;
void enqueueEofPackets();
static void ClearStream(StreamWrap &wrap);
[[nodiscard]] static crl::time CountPacketPositionTime(
not_null<const AVStream*> info,
const Packet &packet);
[[nodiscard]] static crl::time CountPacketPositionTime(
const StreamWrap &wrap,
const Packet &packet);
static void Enqueue(StreamWrap &wrap, Packet &&packet);
not_null<Reader*> _reader;
Mode _mode = Mode::Both;
int _offset = 0; int _offset = 0;
int _size = 0; int _size = 0;
bool _failed = false; bool _failed = false;
@ -113,22 +85,16 @@ private:
bool _readTillEnd = false; bool _readTillEnd = false;
crl::semaphore _semaphore; crl::semaphore _semaphore;
std::atomic<bool> _interrupted = false; std::atomic<bool> _interrupted = false;
std::optional<Information> _information;
uchar *_ioBuffer = nullptr; uchar *_ioBuffer = nullptr;
AVIOContext *_ioContext = nullptr; AVIOContext *_ioContext = nullptr;
AVFormatContext *_formatContext = nullptr; AVFormatContext *_formatContext = nullptr;
StreamWrap _video;
StreamWrap _audio;
AudioMsgId _audioMsgId;
}; };
std::thread _thread; std::optional<Context> _context;
Reader _reader; Reader _reader;
std::unique_ptr<Context> _context; std::thread _thread;
}; };

View file

@ -0,0 +1,29 @@
/*
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 Media {
namespace Streaming {
struct Stream;
class Packet;
class FileDelegate {
public:
virtual void fileReady(Stream &&video, Stream &&audio) = 0;
virtual void fileError() = 0;
// Return true if reading and processing more packets is desired.
// Return false if sleeping until 'wake()' is called is desired.
// Return true after the EOF packet if looping is desired.
[[nodiscard]] virtual bool fileProcessPacket(Packet &&packet) = 0;
[[nodiscard]] virtual bool fileReadMore() = 0;
};
} // namespace Streaming
} // namespace Media

View file

@ -0,0 +1,123 @@
/*
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_player.h"
#include "media/streaming/media_streaming_file.h"
#include "media/streaming/media_streaming_loader.h"
#include "media/audio/media_audio.h"
#include "media/audio/media_child_ffmpeg_loader.h"
namespace Media {
namespace Streaming {
crl::time CountPacketPosition(
not_null<const AVStream*> info,
const Packet &packet) {
const auto &native = packet.fields();
const auto packetPts = (native.pts == AV_NOPTS_VALUE)
? native.dts
: native.pts;
const auto & timeBase = info->time_base;
return PtsToTime(packetPts, info->time_base);
}
// #TODO streaming
//void Enqueue(StreamWrap &wrap, Packet && packet) {
// const auto time = CountPacketPosition(wrap, packet);
// if (time != kTimeUnknown) {
// wrap.stream.lastReadPosition = time;
// }
// wrap.stream.queue.push_back(std::move(packet));
//}
Player::Player(
not_null<Data::Session*> owner,
std::unique_ptr<Loader> loader)
: _file(std::make_unique<File>(owner, std::move(loader))) {
}
not_null<FileDelegate*> Player::delegate() {
return static_cast<FileDelegate*>(this);
}
void Player::fileReady(Stream &&video, Stream &&audio) {
_audio = std::move(audio);
if (_audio.codec && (_mode == Mode::Audio || _mode == Mode::Both)) {
_audioMsgId = AudioMsgId::ForVideo();
} else {
_audioMsgId = AudioMsgId();
}
}
void Player::fileError() {
}
bool Player::fileProcessPacket(Packet &&packet) {
const auto &native = packet.fields();
if (packet.empty()) {
_readTillEnd = true;
} else if (native.stream_index == _audio.index) {
if (_audioMsgId.playId()) {
if (_audio.codec) {
const auto position = PtsToTime(native.pts, _audio.timeBase);
auto data = std::make_unique<VideoSoundData>();
data->context = _audio.codec.release();
data->frequency = _audio.frequency;
data->length = (_audio.duration * data->frequency) / 1000LL;
Media::Player::mixer()->play(_audioMsgId, std::move(data), position);
// #TODO streaming resume when started playing
Media::Player::mixer()->resume(_audioMsgId, true);
}
Media::Player::mixer()->feedFromVideo({ &native, _audioMsgId });
packet.release();
}
//const auto position = PtsToTime(native.pts, stream->timeBase);
}
return fileReadMore();
}
bool Player::fileReadMore() {
// return true if looping.
return !_readTillEnd;
}
void Player::init(Mode mode, crl::time position) {
stop();
_mode = mode;
_file->start(delegate(), position);
}
void Player::pause() {
}
void Player::resume() {
}
void Player::stop() {
_file->stop();
_updates = rpl::event_stream<Update, Error>();
}
bool Player::playing() const {
return false;
}
rpl::producer<Update, Error> Player::updates() const {
return _updates.events();
}
Player::~Player() = default;
} // namespace Streaming
} // namespace Media

View file

@ -0,0 +1,63 @@
/*
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/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_file_delegate.h"
// #TODO streaming move _audio away
#include "media/streaming/media_streaming_utility.h"
namespace Data {
class Session;
} // namespace Data
namespace Media {
namespace Streaming {
class Loader;
class File;
class Player final : private FileDelegate {
public:
Player(not_null<Data::Session*> owner, std::unique_ptr<Loader> loader);
void init(Mode mode, crl::time position);
void pause();
void resume();
void stop();
bool playing() const;
rpl::producer<Update, Error> updates() const;
~Player();
private:
not_null<FileDelegate*> delegate();
void fileReady(Stream &&video, Stream &&audio) override;
void fileError() override;
bool fileProcessPacket(Packet &&packet) override;
bool fileReadMore() override;
const std::unique_ptr<File> _file;
bool _readTillEnd = false;
Mode _mode = Mode::Both;
Stream _audio;
AudioMsgId _audioMsgId;
rpl::event_stream<Update, Error> _updates;
};
} // namespace Streaming
} // namespace Media

View file

@ -23,8 +23,6 @@ class Reader final {
public: public:
Reader(not_null<Data::Session*> owner, std::unique_ptr<Loader> loader); Reader(not_null<Data::Session*> owner, std::unique_ptr<Loader> loader);
static constexpr auto kPartSize = 128 * 1024;
int size() const; int size() const;
bool fill( bool fill(
bytes::span buffer, bytes::span buffer,

View file

@ -5,6 +5,8 @@ the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link: For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "media/streaming/media_streaming_utility.h"
#include "media/streaming/media_streaming_common.h" #include "media/streaming/media_streaming_common.h"
extern "C" { extern "C" {
@ -143,12 +145,18 @@ void LogError(QLatin1String method, AvErrorWrap error) {
).arg(error.text())); ).arg(error.text()));
} }
crl::time PtsToTime(int64_t pts, const AVRational &timeBase) { crl::time PtsToTime(int64_t pts, AVRational timeBase) {
return (pts == AV_NOPTS_VALUE) return (pts == AV_NOPTS_VALUE || !timeBase.den)
? Information::kDurationUnknown ? kTimeUnknown
: ((pts * 1000LL * timeBase.num) / timeBase.den); : ((pts * 1000LL * timeBase.num) / timeBase.den);
} }
int64_t TimeToPts(crl::time time, AVRational timeBase) {
return (time == kTimeUnknown || !timeBase.num)
? AV_NOPTS_VALUE
: (time * timeBase.den) / (1000LL * timeBase.num);
}
int ReadRotationFromMetadata(not_null<AVStream*> stream) { int ReadRotationFromMetadata(not_null<AVStream*> stream) {
const auto tag = av_dict_get(stream->metadata, "rotate", nullptr, 0); const auto tag = av_dict_get(stream->metadata, "rotate", nullptr, 0);
if (tag && *tag->value) { if (tag && *tag->value) {
@ -291,13 +299,13 @@ QImage ConvertFrame(
return QImage(); return QImage();
} }
} }
if (stream.rotation == 180) { //if (stream.rotation == 180) {
storage = std::move(storage).mirrored(true, true); // storage = std::move(storage).mirrored(true, true);
} else if (stream.rotation != 0) { //} else if (stream.rotation != 0) {
auto transform = QTransform(); // auto transform = QTransform();
transform.rotate(stream.rotation); // transform.rotate(stream.rotation);
storage = storage.transformed(transform); // storage = storage.transformed(transform);
} //}
// Read some future packets for audio stream. // Read some future packets for audio stream.
//if (_audioStreamId >= 0) { //if (_audioStreamId >= 0) {
@ -312,7 +320,7 @@ QImage ConvertFrame(
// #TODO streaming // #TODO streaming
ClearFrameMemory(stream.frame.get()); ClearFrameMemory(stream.frame.get());
return std::move(storage); return storage;
} }
} // namespace Streaming } // namespace Streaming

View file

@ -0,0 +1,156 @@
/*
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/streaming/media_streaming_common.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
} // extern "C"
namespace Media {
namespace Streaming {
constexpr auto kUniversalTimeBase = AVRational{ 1, AV_TIME_BASE };
class AvErrorWrap {
public:
AvErrorWrap(int code = 0) : _code(code) {
}
[[nodiscard]] explicit operator bool() const {
return (_code < 0);
}
[[nodiscard]] int code() const {
return _code;
}
[[nodiscard]] QString text() const {
char string[AV_ERROR_MAX_STRING_SIZE] = { 0 };
return QString::fromUtf8(av_make_error_string(
string,
sizeof(string),
_code));
}
private:
int _code = 0;
};
class Packet {
public:
Packet() {
setEmpty();
}
Packet(const AVPacket &data) {
bytes::copy(_data, bytes::object_as_span(&data));
}
Packet(Packet &&other) {
bytes::copy(_data, other._data);
if (!other.empty()) {
other.release();
}
}
Packet &operator=(Packet &&other) {
if (this != &other) {
av_packet_unref(&fields());
bytes::copy(_data, other._data);
if (!other.empty()) {
other.release();
}
}
return *this;
}
~Packet() {
av_packet_unref(&fields());
}
[[nodiscard]] AVPacket &fields() {
return *reinterpret_cast<AVPacket*>(_data);
}
[[nodiscard]] const AVPacket &fields() const {
return *reinterpret_cast<const AVPacket*>(_data);
}
[[nodiscard]] bool empty() const {
return !fields().data;
}
void release() {
setEmpty();
}
private:
void setEmpty() {
auto &native = fields();
av_init_packet(&native);
native.data = nullptr;
native.size = 0;
}
alignas(alignof(AVPacket)) bytes::type _data[sizeof(AVPacket)];
};
struct CodecDeleter {
void operator()(AVCodecContext *value);
};
using CodecPointer = std::unique_ptr<AVCodecContext, CodecDeleter>;
CodecPointer MakeCodecPointer(not_null<AVStream*> stream);
struct FrameDeleter {
void operator()(AVFrame *value);
};
using FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;
FramePointer MakeFramePointer();
struct SwsContextDeleter {
void operator()(SwsContext *value);
};
using SwsContextPointer = std::unique_ptr<SwsContext, SwsContextDeleter>;
SwsContextPointer MakeSwsContextPointer(
not_null<AVFrame*> frame,
QSize resize,
SwsContextPointer *existing = nullptr);
struct Stream {
int index = -1;
crl::time duration = kTimeUnknown;
AVRational timeBase = kUniversalTimeBase;
CodecPointer codec;
FramePointer frame;
std::deque<Packet> queue;
crl::time lastReadPosition = 0;
int invalidDataPackets = 0;
// Audio only.
int frequency = 0;
// Video only.
int rotation = 0;
SwsContextPointer swsContext;
};
void LogError(QLatin1String method);
void LogError(QLatin1String method, AvErrorWrap error);
[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);
[[nodiscard]] int64_t TimeToPts(int64_t pts, AVRational timeBase);
[[nodiscard]] int ReadRotationFromMetadata(not_null<AVStream*> stream);
[[nodiscard]] bool RotationSwapWidthHeight(int rotation);
[[nodiscard]] std::optional<AvErrorWrap> ReadNextFrame(Stream &stream);
[[nodiscard]] QImage ConvertFrame(
Stream& stream,
QSize resize,
QImage storage);
} // namespace Streaming
} // namespace Media

View file

@ -454,16 +454,20 @@
<(src_loc)/media/player/media_player_volume_controller.h <(src_loc)/media/player/media_player_volume_controller.h
<(src_loc)/media/player/media_player_widget.cpp <(src_loc)/media/player/media_player_widget.cpp
<(src_loc)/media/player/media_player_widget.h <(src_loc)/media/player/media_player_widget.h
<(src_loc)/media/streaming/media_streaming_common.cpp
<(src_loc)/media/streaming/media_streaming_common.h <(src_loc)/media/streaming/media_streaming_common.h
<(src_loc)/media/streaming/media_streaming_file.cpp <(src_loc)/media/streaming/media_streaming_file.cpp
<(src_loc)/media/streaming/media_streaming_file.h <(src_loc)/media/streaming/media_streaming_file.h
<(src_loc)/media/streaming/media_streaming_file_delegate.h
<(src_loc)/media/streaming/media_streaming_loader.cpp <(src_loc)/media/streaming/media_streaming_loader.cpp
<(src_loc)/media/streaming/media_streaming_loader.h <(src_loc)/media/streaming/media_streaming_loader.h
<(src_loc)/media/streaming/media_streaming_loader_mtproto.cpp <(src_loc)/media/streaming/media_streaming_loader_mtproto.cpp
<(src_loc)/media/streaming/media_streaming_loader_mtproto.h <(src_loc)/media/streaming/media_streaming_loader_mtproto.h
<(src_loc)/media/streaming/media_streaming_player.cpp
<(src_loc)/media/streaming/media_streaming_player.h
<(src_loc)/media/streaming/media_streaming_reader.cpp <(src_loc)/media/streaming/media_streaming_reader.cpp
<(src_loc)/media/streaming/media_streaming_reader.h <(src_loc)/media/streaming/media_streaming_reader.h
<(src_loc)/media/streaming/media_streaming_utility.cpp
<(src_loc)/media/streaming/media_streaming_utility.h
<(src_loc)/media/view/media_clip_controller.cpp <(src_loc)/media/view/media_clip_controller.cpp
<(src_loc)/media/view/media_clip_controller.h <(src_loc)/media/view/media_clip_controller.h
<(src_loc)/media/view/media_clip_playback.cpp <(src_loc)/media/view/media_clip_playback.cpp