mirror of
https://github.com/vale981/tdesktop
synced 2025-03-06 10:11:41 -05:00
Move some logic to Media::Streaming::Player.
This commit is contained in:
parent
64f2f330f6
commit
a093cb6274
12 changed files with 633 additions and 493 deletions
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()) {
|
|
||||||
return QImage();
|
|
||||||
}
|
|
||||||
frame.match([&](QImage &image) {
|
|
||||||
if (!image.isNull()) {
|
|
||||||
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() {
|
|
||||||
Expects(_video.info != nullptr);
|
|
||||||
|
|
||||||
if (unroll()) {
|
if (unroll()) {
|
||||||
return AvErrorWrap();
|
return;
|
||||||
}
|
}
|
||||||
const auto error = ReadNextFrame(_video.stream);
|
|
||||||
if (error) {
|
_delegate->fileReady(std::move(video), std::move(audio));
|
||||||
if (error->code() == AVERROR_EOF) {
|
|
||||||
// No valid video stream.
|
|
||||||
if (_mode == Mode::Video) {
|
|
||||||
logFatal(qstr("RequiredStreamEmpty"));
|
|
||||||
}
|
|
||||||
return QImage();
|
|
||||||
} else if (error->code() != AVERROR(EAGAIN)) {
|
|
||||||
_failed = true;
|
|
||||||
}
|
|
||||||
return *error;
|
|
||||||
}
|
|
||||||
return ConvertFrame(_video.stream, QSize(), QImage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void File::Context::enqueueEofPackets() {
|
//void File::Context::readInformation(crl::time position) {
|
||||||
if (_audio.info) {
|
// auto information = Information();
|
||||||
Enqueue(_audio, Packet());
|
// auto result = readPacket();
|
||||||
}
|
// const auto packet = base::get_if<Packet>(&result);
|
||||||
if (_video.info) {
|
// if (unroll()) {
|
||||||
Enqueue(_video, Packet());
|
// return;
|
||||||
}
|
// } else if (packet) {
|
||||||
_readTillEnd = true;
|
// if (position > 0) {
|
||||||
}
|
// const auto time = CountPacketPosition(
|
||||||
|
// _formatContext->streams[packet->fields().stream_index],
|
||||||
void File::Context::processPacket(Packet &&packet) {
|
// *packet);
|
||||||
const auto &native = packet.fields();
|
// information.started = (time == Information::kDurationUnknown)
|
||||||
const auto streamId = native.stream_index;
|
// ? position
|
||||||
const auto check = [&](StreamWrap &wrap) {
|
// : time;
|
||||||
if ((native.stream_index == wrap.id) && wrap.info) {
|
// }
|
||||||
// #TODO streaming queue packet to audio player
|
// } else {
|
||||||
if ((_mode == Mode::Audio || _mode == Mode::Both)
|
// information.started = position;
|
||||||
&& (wrap.info == _audio.info)) {
|
// }
|
||||||
Player::mixer()->feedFromVideo({ &native, _audioMsgId });
|
//
|
||||||
packet.release();
|
// if (packet) {
|
||||||
} else {
|
// processPacket(std::move(*packet));
|
||||||
Enqueue(wrap, std::move(packet));
|
// } else {
|
||||||
}
|
// enqueueEofPackets();
|
||||||
return true;
|
// }
|
||||||
}
|
//
|
||||||
return false;
|
// information.cover = readFirstVideoFrame();
|
||||||
};
|
// if (unroll()) {
|
||||||
|
// return;
|
||||||
check(_audio) || check(_video);
|
// } 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);
|
||||||
|
// _information = std::move(information);
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//QImage File::Context::readFirstVideoFrame() {
|
||||||
|
// auto result = QImage();
|
||||||
|
// while (_video.info && result.isNull()) {
|
||||||
|
// auto frame = tryReadFirstVideoFrame();
|
||||||
|
// if (unroll()) {
|
||||||
|
// return QImage();
|
||||||
|
// }
|
||||||
|
// frame.match([&](QImage &image) {
|
||||||
|
// if (!image.isNull()) {
|
||||||
|
// result = std::move(image);
|
||||||
|
// } 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()) {
|
while (!context->finished()) {
|
||||||
return;
|
context->readNextPacket();
|
||||||
} 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()) {
|
|
||||||
context->readNextPacket();
|
|
||||||
}
|
|
||||||
crl::on_main(context, [] { AssertIsDebug();
|
|
||||||
Ui::Toast::Show("Finished loading.");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crl::on_main(context, [] { AssertIsDebug();
|
||||||
|
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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
123
Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
Normal file
123
Telegram/SourceFiles/media/streaming/media_streaming_player.cpp
Normal 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
|
|
@ -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
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
156
Telegram/SourceFiles/media/streaming/media_streaming_utility.h
Normal file
156
Telegram/SourceFiles/media/streaming/media_streaming_utility.h
Normal 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
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue