Show option to download on streaming error.

This commit is contained in:
John Preston 2019-03-04 15:28:52 +04:00
parent 003d01206f
commit dafa286b18
17 changed files with 262 additions and 169 deletions

View File

@ -1492,6 +1492,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_player_message_today" = "Today at {time}"; "lng_player_message_today" = "Today at {time}";
"lng_player_message_yesterday" = "Yesterday at {time}"; "lng_player_message_yesterday" = "Yesterday at {time}";
"lng_player_message_date" = "{date} at {time}"; "lng_player_message_date" = "{date} at {time}";
"lng_player_cant_play" = "This file can't be played in Telegram Desktop.\n\nWould you like to download it and open it in an external player?";
"lng_player_download" = "Download";
"lng_rights_edit_admin" = "Manage permissions"; "lng_rights_edit_admin" = "Manage permissions";
"lng_rights_edit_admin_header" = "What can this admin do?"; "lng_rights_edit_admin_header" = "What can this admin do?";

View File

@ -300,6 +300,9 @@ void DocumentOpenClickHandler::Open(
Core::App().showDocument(data, context); Core::App().showDocument(data, context);
location.accessDisable(); location.accessDisable();
return; return;
} else if (data->inappPlaybackFailed()) {
::Data::HandleUnsupportedMedia(data, msgId);
return;
} else if (data->canBePlayed()) { } else if (data->canBePlayed()) {
if (data->isAudioFile() || data->isVoiceMessage()) { if (data->isAudioFile() || data->isVoiceMessage()) {
Media::Player::instance()->playPause({ data, msgId }); Media::Player::instance()->playPause({ data, msgId });
@ -705,55 +708,32 @@ void DocumentData::performActionOnLoad() {
return; return;
} }
auto loc = location(true); const auto loc = location(true);
auto already = loc.name(); const auto &already = loc.name();
auto item = _actionOnLoadMsgId.msg ? App::histItemById(_actionOnLoadMsgId) : nullptr; const auto item = _actionOnLoadMsgId.msg
auto showImage = !isVideoFile() && (size < App::kImageSizeLimit); ? App::histItemById(_actionOnLoadMsgId)
auto playVoice = isVoiceMessage() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen); : nullptr;
auto playMusic = isAudioFile() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen); const auto showImage = !isVideoFile() && (size < App::kImageSizeLimit);
auto playAnimation = isAnimation() const auto playVoice = isVoiceMessage();
&& (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen) const auto playMusic = isAudioFile();
const auto playAnimation = isAnimation()
&& loaded()
&& showImage && showImage
&& item; && item;
if (auto applyTheme = isTheme()) { if (isTheme()) {
if (!loc.isEmpty() && loc.accessEnable()) { if (!loc.isEmpty() && loc.accessEnable()) {
Core::App().showDocument(this, item); Core::App().showDocument(this, item);
loc.accessDisable(); loc.accessDisable();
return;
} }
} } else if (_actionOnLoad == ActionOnLoadOpenWith) {
if (playVoice || playMusic) { if (!already.isEmpty()) {
DocumentOpenClickHandler::Open({}, this, item, ActionOnLoadNone);
} else if (playAnimation) {
if (loaded()) {
if (_actionOnLoad == ActionOnLoadPlayInline && item) {
_owner->requestAnimationPlayInline(item);
} else {
Core::App().showDocument(this, item);
}
}
} else {
if (already.isEmpty()) return;
if (_actionOnLoad == ActionOnLoadOpenWith) {
File::OpenWith(already, QCursor::pos()); File::OpenWith(already, QCursor::pos());
} else if (_actionOnLoad == ActionOnLoadOpen || _actionOnLoad == ActionOnLoadPlayInline) {
if (isVoiceMessage() || isAudioFile() || isVideoFile()) {
if (Data::IsValidMediaFile(already)) {
File::Launch(already);
}
_owner->markMediaRead(this);
} else if (loc.accessEnable()) {
if (showImage && QImageReader(loc.name()).canRead()) {
Core::App().showDocument(this, item);
} else {
LaunchWithWarning(already, item);
}
loc.accessDisable();
} else {
LaunchWithWarning(already, item);
}
} }
} else if (playVoice
|| playMusic
|| playAnimation
|| !already.isEmpty()) {
DocumentOpenClickHandler::Open({}, this, item, _actionOnLoad);
} }
_actionOnLoad = ActionOnLoadNone; _actionOnLoad = ActionOnLoadNone;
} }
@ -1222,6 +1202,14 @@ bool DocumentData::canBePlayed() const {
&& (loaded() || canBeStreamed()); && (loaded() || canBeStreamed());
} }
void DocumentData::setInappPlaybackFailed() {
_inappPlaybackFailed = true;
}
bool DocumentData::inappPlaybackFailed() const {
return _inappPlaybackFailed;
}
auto DocumentData::createStreamingLoader(Data::FileOrigin origin) const auto DocumentData::createStreamingLoader(Data::FileOrigin origin) const
-> std::unique_ptr<Media::Streaming::Loader> { -> std::unique_ptr<Media::Streaming::Loader> {
// #TODO streaming create local file loader // #TODO streaming create local file loader
@ -1596,4 +1584,28 @@ base::binary_guard ReadImageAsync(
return std::move(right); return std::move(right);
} }
void HandleUnsupportedMedia(
not_null<DocumentData*> document,
FullMsgId contextId) {
document->setInappPlaybackFailed();
auto filepath = document->filepath(
DocumentData::FilePathResolveSaveFromData);
if (filepath.isEmpty()) {
const auto save = [=] {
Ui::hideLayer();
DocumentSaveClickHandler::Save(
(contextId ? contextId : Data::FileOrigin()),
document,
App::histItemById(contextId));
};
Ui::show(Box<ConfirmBox>(
lang(lng_player_cant_play),
lang(lng_player_download),
lang(lng_cancel),
save));
} else if (IsValidMediaFile(filepath)) {
File::Launch(filepath);
}
}
} // namespace Data } // namespace Data

View File

@ -228,6 +228,9 @@ public:
[[nodiscard]] auto createStreamingLoader(Data::FileOrigin origin) const [[nodiscard]] auto createStreamingLoader(Data::FileOrigin origin) const
-> std::unique_ptr<Media::Streaming::Loader>; -> std::unique_ptr<Media::Streaming::Loader>;
void setInappPlaybackFailed();
[[nodiscard]] bool inappPlaybackFailed() const;
~DocumentData(); ~DocumentData();
DocumentId id = 0; DocumentId id = 0;
@ -272,6 +275,7 @@ private:
int32 _duration = -1; int32 _duration = -1;
bool _isImage = false; bool _isImage = false;
bool _supportsStreaming = false; bool _supportsStreaming = false;
bool _inappPlaybackFailed = false;
ActionOnLoad _actionOnLoad = ActionOnLoadNone; ActionOnLoad _actionOnLoad = ActionOnLoadNone;
FullMsgId _actionOnLoadMsgId; FullMsgId _actionOnLoadMsgId;
@ -393,4 +397,8 @@ base::binary_guard ReadImageAsync(
FnMut<QImage(QImage)> postprocess, FnMut<QImage(QImage)> postprocess,
FnMut<void(QImage&&)> done); FnMut<void(QImage&&)> done);
void HandleUnsupportedMedia(
not_null<DocumentData*> document,
FullMsgId contextId);
} // namespace Data } // namespace Data

View File

@ -389,11 +389,9 @@ MainWidget::MainWidget(
connect(_dialogs, SIGNAL(cancelled()), this, SLOT(dialogsCancelled())); connect(_dialogs, SIGNAL(cancelled()), this, SLOT(dialogsCancelled()));
connect(this, SIGNAL(dialogsUpdated()), _dialogs, SLOT(onListScroll())); connect(this, SIGNAL(dialogsUpdated()), _dialogs, SLOT(onListScroll()));
connect(_history, SIGNAL(cancelled()), _dialogs, SLOT(activate())); connect(_history, SIGNAL(cancelled()), _dialogs, SLOT(activate()));
subscribe(Media::Player::Updated(), [this](const AudioMsgId &audioId) { subscribe(
if (audioId.type() != AudioMsgId::Type::Video) { Media::Player::instance()->updatedNotifier(),
handleAudioUpdate(audioId); [=](const Media::Player::TrackState &state) { handleAudioUpdate(state); });
}
});
subscribe(session().calls().currentCallChanged(), [this](Calls::Call *call) { setCurrentCall(call); }); subscribe(session().calls().currentCallChanged(), [this](Calls::Call *call) { setCurrentCall(call); });
session().data().currentExportView( session().data().currentExportView(
@ -1173,29 +1171,17 @@ void MainWidget::messagesAffected(
} }
} }
void MainWidget::handleAudioUpdate(const AudioMsgId &audioId) { void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) {
using State = Media::Player::State; using State = Media::Player::State;
const auto document = audioId.audio(); const auto document = state.id.audio();
auto state = Media::Player::instance()->getState(audioId.type()); if (!Media::Player::IsStoppedOrStopping(state.state)) {
if (state.id == audioId && state.state == State::StoppedAtStart) { createPlayer();
state.state = State::Stopped; } else if (state.state == State::StoppedAtStart) {
Media::Player::mixer()->clearStoppedAtStart(audioId); Data::HandleUnsupportedMedia(document, state.id.contextId());
closeBothPlayers();
auto filepath = document->filepath(DocumentData::FilePathResolveSaveFromData);
if (!filepath.isEmpty()) {
if (Data::IsValidMediaFile(filepath)) {
File::Launch(filepath);
}
}
} }
if (state.id == audioId) { if (const auto item = App::histItemById(state.id.contextId())) {
if (!Media::Player::IsStoppedOrStopping(state.state)) {
createPlayer();
}
}
if (const auto item = App::histItemById(audioId.contextId())) {
session().data().requestItemRepaint(item); session().data().requestItemRepaint(item);
} }
if (const auto items = InlineBots::Layout::documentItems()) { if (const auto items = InlineBots::Layout::documentItems()) {

View File

@ -42,6 +42,7 @@ namespace Player {
class Widget; class Widget;
class VolumeWidget; class VolumeWidget;
class Panel; class Panel;
struct TrackState;
} // namespace Player } // namespace Player
} // namespace Media } // namespace Media
@ -350,7 +351,7 @@ private:
void animationCallback(); void animationCallback();
void handleAdaptiveLayoutUpdate(); void handleAdaptiveLayoutUpdate();
void updateWindowAdaptiveLayout(); void updateWindowAdaptiveLayout();
void handleAudioUpdate(const AudioMsgId &audioId); void handleAudioUpdate(const Media::Player::TrackState &state);
void updateMediaPlayerPosition(); void updateMediaPlayerPosition();
void updateMediaPlaylistPosition(int x); void updateMediaPlaylistPosition(int x);
void updateControlsGeometry(); void updateControlsGeometry();

View File

@ -1247,14 +1247,6 @@ void Mixer::setStoppedState(Track *current, State state) {
} }
} }
void Mixer::clearStoppedAtStart(const AudioMsgId &audio) {
QMutexLocker lock(&AudioMutex);
auto track = trackForType(audio.type());
if (track && track->state.id == audio && track->state.state == State::StoppedAtStart) {
setStoppedState(track);
}
}
// Thread: Main. Must be locked: AudioMutex. // Thread: Main. Must be locked: AudioMutex.
void Mixer::detachTracks() { void Mixer::detachTracks() {
for (auto i = 0; i != kTogetherLimit; ++i) { for (auto i = 0; i != kTogetherLimit; ++i) {

View File

@ -153,8 +153,6 @@ public:
TrackState currentState(AudioMsgId::Type type); TrackState currentState(AudioMsgId::Type type);
void clearStoppedAtStart(const AudioMsgId &audio);
// Thread: Main. Must be locked: AudioMutex. // Thread: Main. Must be locked: AudioMutex.
void detachTracks(); void detachTracks();

View File

@ -327,7 +327,9 @@ void Instance::play(AudioMsgId::Type type) {
if (IsStopped(state.state)) { if (IsStopped(state.state)) {
play(state.id); play(state.id);
} else if (data->streamed) { } else if (data->streamed) {
data->streamed->player.resume(); if (data->streamed->player.active()) {
data->streamed->player.resume();
}
emitUpdate(type); emitUpdate(type);
} else { } else {
mixer()->resume(state.id); mixer()->resume(state.id);
@ -414,7 +416,9 @@ Streaming::PlaybackOptions Instance::streamingOptions(
void Instance::pause(AudioMsgId::Type type) { void Instance::pause(AudioMsgId::Type type) {
if (const auto data = getData(type)) { if (const auto data = getData(type)) {
if (data->streamed) { if (data->streamed) {
data->streamed->player.pause(); if (data->streamed->player.active()) {
data->streamed->player.pause();
}
emitUpdate(type); emitUpdate(type);
} else { } else {
const auto state = getState(type); const auto state = getState(type);
@ -442,12 +446,9 @@ void Instance::stop(AudioMsgId::Type type) {
void Instance::playPause(AudioMsgId::Type type) { void Instance::playPause(AudioMsgId::Type type) {
if (const auto data = getData(type)) { if (const auto data = getData(type)) {
if (data->streamed) { if (data->streamed) {
if (data->streamed->player.finished() if (!data->streamed->player.active()) {
|| data->streamed->player.failed()) { data->streamed->player.play(
auto options = Streaming::PlaybackOptions(); streamingOptions(data->streamed->id));
options.mode = Streaming::Mode::Audio;
options.audioId = data->streamed->id;
data->streamed->player.play(options);
} else if (data->streamed->player.paused()) { } else if (data->streamed->player.paused()) {
data->streamed->player.resume(); data->streamed->player.resume();
} else { } else {
@ -702,12 +703,19 @@ void Instance::handleStreamingUpdate(
}; };
finishTrack(data->streamed->info.audio.state); finishTrack(data->streamed->info.audio.state);
emitUpdate(data->type); emitUpdate(data->type);
if (data->streamed && data->streamed->player.finished()) {
data->streamed = nullptr;
}
}); });
} }
void Instance::handleStreamingError( void Instance::handleStreamingError(
not_null<Data*> data, not_null<Data*> data,
Streaming::Error &&error) { Streaming::Error &&error) {
emitUpdate(data->type);
if (data->streamed && data->streamed->player.failed()) {
data->streamed = nullptr;
}
} }
} // namespace Player } // namespace Player

View File

@ -98,7 +98,7 @@ void File::Context::logFatal(QLatin1String method, AvErrorWrap error) {
Stream File::Context::initStream(AVMediaType type) { Stream File::Context::initStream(AVMediaType type) {
auto result = Stream(); auto result = Stream();
const auto index = result.index = av_find_best_stream( const auto index = result.index = av_find_best_stream(
_formatContext, _format.get(),
type, type,
-1, -1,
-1, -1,
@ -108,7 +108,7 @@ Stream File::Context::initStream(AVMediaType type) {
return {}; return {};
} }
const auto info = _formatContext->streams[index]; const auto info = _format->streams[index];
if (type == AVMEDIA_TYPE_VIDEO) { if (type == AVMEDIA_TYPE_VIDEO) {
result.rotation = ReadRotationFromMetadata(info); result.rotation = ReadRotationFromMetadata(info);
} else if (type == AVMEDIA_TYPE_AUDIO) { } else if (type == AVMEDIA_TYPE_AUDIO) {
@ -130,7 +130,7 @@ Stream File::Context::initStream(AVMediaType type) {
result.timeBase = info->time_base; result.timeBase = info->time_base;
result.duration = (info->duration != AV_NOPTS_VALUE) result.duration = (info->duration != AV_NOPTS_VALUE)
? PtsToTimeCeil(info->duration, result.timeBase) ? PtsToTimeCeil(info->duration, result.timeBase)
: PtsToTimeCeil(_formatContext->duration, kUniversalTimeBase); : PtsToTimeCeil(_format->duration, kUniversalTimeBase);
if (result.duration == kTimeUnknown || !result.duration) { if (result.duration == kTimeUnknown || !result.duration) {
return {}; return {};
} }
@ -151,7 +151,7 @@ void File::Context::seekToPosition(
// //
//const auto seekFlags = 0; //const auto seekFlags = 0;
//error = av_seek_frame( //error = av_seek_frame(
// _formatContext, // _format,
// streamIndex, // streamIndex,
// TimeToPts(position, kUniversalTimeBase), // TimeToPts(position, kUniversalTimeBase),
// seekFlags); // seekFlags);
@ -160,7 +160,7 @@ void File::Context::seekToPosition(
//} //}
// //
error = av_seek_frame( error = av_seek_frame(
_formatContext, _format.get(),
stream.index, stream.index,
TimeToPts( TimeToPts(
std::clamp(position, crl::time(0), stream.duration - 1), std::clamp(position, crl::time(0), stream.duration - 1),
@ -176,7 +176,7 @@ base::variant<Packet, AvErrorWrap> File::Context::readPacket() {
auto error = AvErrorWrap(); auto error = AvErrorWrap();
auto result = Packet(); auto result = Packet();
error = av_read_frame(_formatContext, &result.fields()); error = av_read_frame(_format.get(), &result.fields());
if (unroll()) { if (unroll()) {
return AvErrorWrap(); return AvErrorWrap();
} else if (!error) { } else if (!error) {
@ -193,37 +193,16 @@ void File::Context::start(crl::time position) {
if (unroll()) { if (unroll()) {
return; return;
} }
_ioBuffer = reinterpret_cast<uchar*>(av_malloc(AVBlockSize)); _format = MakeFormatPointer(
_ioContext = avio_alloc_context( static_cast<void *>(this),
_ioBuffer,
AVBlockSize,
0,
static_cast<void*>(this),
&Context::Read, &Context::Read,
nullptr, nullptr,
&Context::Seek); &Context::Seek);
_formatContext = avformat_alloc_context(); if (!_format) {
if (!_formatContext) { return fail();
return logFatal(qstr("avformat_alloc_context"));
} }
_formatContext->pb = _ioContext;
auto options = (AVDictionary*)nullptr; if ((error = avformat_find_stream_info(_format.get(), nullptr))) {
const auto guard = gsl::finally([&] { av_dict_free(&options); });
av_dict_set(&options, "usetoc", "1", 0);
error = avformat_open_input(
&_formatContext,
nullptr,
nullptr,
&options);
if (error) {
_ioBuffer = nullptr;
return logFatal(qstr("avformat_open_input"), error);
}
_opened = true;
_formatContext->flags |= AVFMT_FLAG_FAST_SEEK;
if ((error = avformat_find_stream_info(_formatContext, nullptr))) {
return logFatal(qstr("avformat_find_stream_info"), error); return logFatal(qstr("avformat_find_stream_info"), error);
} }
@ -299,20 +278,7 @@ void File::Context::fail() {
_delegate->fileError(); _delegate->fileError();
} }
File::Context::~Context() { File::Context::~Context() = default;
if (_opened) {
avformat_close_input(&_formatContext);
}
if (_ioContext) {
av_freep(&_ioContext->buffer);
av_freep(&_ioContext);
} else if (_ioBuffer) {
av_freep(&_ioBuffer);
}
if (_formatContext) {
avformat_free_context(_formatContext);
}
}
bool File::Context::finished() const { bool File::Context::finished() const {
// #TODO streaming later looping // #TODO streaming later looping
@ -348,6 +314,7 @@ void File::stop() {
_context->interrupt(); _context->interrupt();
_thread.join(); _thread.join();
} }
_reader.stop();
_context.reset(); _context.reset();
} }

View File

@ -87,9 +87,7 @@ private:
crl::semaphore _semaphore; crl::semaphore _semaphore;
std::atomic<bool> _interrupted = false; std::atomic<bool> _interrupted = false;
uchar *_ioBuffer = nullptr; FormatPointer _format;
AVIOContext *_ioContext = nullptr;
AVFormatContext *_formatContext = nullptr;
}; };

View File

@ -355,7 +355,7 @@ void Player::provideStartInformation() {
void Player::fail() { void Player::fail() {
_sessionLifetime = rpl::lifetime(); _sessionLifetime = rpl::lifetime();
const auto stopGuarded = crl::guard(&_sessionGuard, [=] { stop(); }); const auto stopGuarded = crl::guard(&_sessionGuard, [=] { stop(); });
_stage = Stage::Failed; _lastFailureStage = _stage;
_updates.fire_error({}); _updates.fire_error({});
stopGuarded(); stopGuarded();
} }
@ -364,6 +364,7 @@ void Player::play(const PlaybackOptions &options) {
Expects(options.speed >= 0.5 && options.speed <= 2.); Expects(options.speed >= 0.5 && options.speed <= 2.);
stop(); stop();
_lastFailureStage = Stage::Uninitialized;
_options = options; _options = options;
if (!Media::Audio::SupportsSpeedControl()) { if (!Media::Audio::SupportsSpeedControl()) {
@ -374,14 +375,14 @@ void Player::play(const PlaybackOptions &options) {
} }
void Player::pause() { void Player::pause() {
Expects(valid()); Expects(active());
_pausedByUser = true; _pausedByUser = true;
updatePausedState(); updatePausedState();
} }
void Player::resume() { void Player::resume() {
Expects(valid()); Expects(active());
_pausedByUser = false; _pausedByUser = false;
updatePausedState(); updatePausedState();
@ -510,9 +511,7 @@ void Player::start() {
void Player::stop() { void Player::stop() {
_file->stop(); _file->stop();
_sessionLifetime = rpl::lifetime(); _sessionLifetime = rpl::lifetime();
if (_stage != Stage::Failed) { _stage = Stage::Uninitialized;
_stage = Stage::Uninitialized;
}
_audio = nullptr; _audio = nullptr;
_video = nullptr; _video = nullptr;
invalidate_weak_ptrs(&_sessionGuard); invalidate_weak_ptrs(&_sessionGuard);
@ -526,11 +525,14 @@ void Player::stop() {
} }
bool Player::failed() const { bool Player::failed() const {
return (_stage == Stage::Failed); return (_lastFailureStage != Stage::Uninitialized);
} }
bool Player::playing() const { bool Player::playing() const {
return (_stage == Stage::Started) && !_paused && !finished(); return (_stage == Stage::Started)
&& !paused()
&& !finished()
&& !failed();
} }
bool Player::buffering() const { bool Player::buffering() const {
@ -548,7 +550,7 @@ bool Player::finished() const {
} }
void Player::setSpeed(float64 speed) { void Player::setSpeed(float64 speed) {
Expects(valid()); Expects(active());
Expects(speed >= 0.5 && speed <= 2.); Expects(speed >= 0.5 && speed <= 2.);
if (!Media::Audio::SupportsSpeedControl()) { if (!Media::Audio::SupportsSpeedControl()) {
@ -565,12 +567,12 @@ void Player::setSpeed(float64 speed) {
} }
} }
bool Player::valid() const { bool Player::active() const {
return (_stage != Stage::Uninitialized) && (_stage != Stage::Failed); return (_stage != Stage::Uninitialized) && !finished() && !failed();
} }
bool Player::ready() const { bool Player::ready() const {
return valid() && (_stage != Stage::Initializing); return (_stage != Stage::Uninitialized) && (_stage != Stage::Initializing);
} }
rpl::producer<Update, Error> Player::updates() const { rpl::producer<Update, Error> Player::updates() const {
@ -588,7 +590,11 @@ Media::Player::TrackState Player::prepareLegacyState() const {
auto result = Media::Player::TrackState(); auto result = Media::Player::TrackState();
result.id = _audioId.externalPlayId() ? _audioId : _options.audioId; result.id = _audioId.externalPlayId() ? _audioId : _options.audioId;
result.state = finished() result.state = (_lastFailureStage == Stage::Started)
? State::StoppedAtError
: failed()
? State::StoppedAtStart
: finished()
? State::StoppedAtEnd ? State::StoppedAtEnd
: paused() : paused()
? State::Paused ? State::Paused
@ -609,6 +615,8 @@ Media::Player::TrackState Player::prepareLegacyState() const {
: document->duration(); : document->duration();
if (duration > 0) { if (duration > 0) {
result.length = duration * crl::time(1000); result.length = duration * crl::time(1000);
} else {
result.length = std::max(result.position, crl::time(0));
} }
} }
result.frequency = kMsFrequency; result.frequency = kMsFrequency;

View File

@ -44,10 +44,10 @@ public:
void resume(); void resume();
void stop(); void stop();
bool valid() const; [[nodiscard]] bool active() const;
bool ready() const; [[nodiscard]] bool ready() const;
float64 speed() const; [[nodiscard]] float64 speed() const;
void setSpeed(float64 speed); // 0.5 <= speed <= 2. void setSpeed(float64 speed); // 0.5 <= speed <= 2.
[[nodiscard]] bool playing() const; [[nodiscard]] bool playing() const;
@ -72,7 +72,6 @@ private:
Initializing, Initializing,
Ready, Ready,
Started, Started,
Failed
}; };
// Thread-safe. // Thread-safe.
@ -143,6 +142,7 @@ private:
// Belongs to the main thread. // Belongs to the main thread.
Information _information; Information _information;
Stage _stage = Stage::Uninitialized; Stage _stage = Stage::Uninitialized;
Stage _lastFailureStage = Stage::Uninitialized;
bool _pausedByUser = false; bool _pausedByUser = false;
bool _pausedByWaitingForData = false; bool _pausedByWaitingForData = false;
bool _paused = false; bool _paused = false;

View File

@ -540,6 +540,10 @@ Reader::Reader(
} }
} }
void Reader::stop() {
_waiting = nullptr;
}
std::shared_ptr<Reader::CacheHelper> Reader::InitCacheHelper( std::shared_ptr<Reader::CacheHelper> Reader::InitCacheHelper(
std::optional<Storage::Cache::Key> baseKey) { std::optional<Storage::Cache::Key> baseKey) {
if (!baseKey) { if (!baseKey) {

View File

@ -39,6 +39,8 @@ public:
void headerDone(); void headerDone();
void stop();
~Reader(); ~Reader();
private: private:

View File

@ -22,6 +22,7 @@ constexpr auto kSkipInvalidDataPackets = 10;
constexpr auto kAlignImageBy = 16; constexpr auto kAlignImageBy = 16;
constexpr auto kPixelBytesSize = 4; constexpr auto kPixelBytesSize = 4;
constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied; constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied;
constexpr auto kAvioBlockSize = 4096;
void AlignedImageBufferCleanupHandler(void* data) { void AlignedImageBufferCleanupHandler(void* data) {
const auto buffer = static_cast<uchar*>(data); const auto buffer = static_cast<uchar*>(data);
@ -68,6 +69,82 @@ QImage CreateFrameStorage(QSize size) {
cleanupData); cleanupData);
} }
IOPointer MakeIOPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
auto buffer = reinterpret_cast<uchar*>(av_malloc(kAvioBlockSize));
if (!buffer) {
LogError(qstr("av_malloc"));
return {};
}
auto result = IOPointer(avio_alloc_context(
buffer,
kAvioBlockSize,
write ? 1 : 0,
opaque,
read,
write,
seek));
if (!result) {
av_freep(&buffer);
LogError(qstr("avio_alloc_context"));
return {};
}
return result;
}
void IODeleter::operator()(AVIOContext *value) {
if (value) {
av_freep(&value->buffer);
avio_context_free(&value);
}
}
FormatPointer MakeFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
int64_t(*seek)(void *opaque, int64_t offset, int whence)) {
auto io = MakeIOPointer(opaque, read, write, seek);
if (!io) {
return {};
}
auto result = avformat_alloc_context();
if (!result) {
LogError(qstr("avformat_alloc_context"));
return {};
}
result->pb = io.get();
auto options = (AVDictionary*)nullptr;
const auto guard = gsl::finally([&] { av_dict_free(&options); });
av_dict_set(&options, "usetoc", "1", 0);
const auto error = AvErrorWrap(avformat_open_input(
&result,
nullptr,
nullptr,
&options));
if (error) {
// avformat_open_input freed 'result' in case an error happened.
LogError(qstr("avformat_open_input"), error);
return {};
}
result->flags |= AVFMT_FLAG_FAST_SEEK;
// Now FormatPointer will own and free the IO context.
io.release();
return FormatPointer(result);
}
void FormatDeleter::operator()(AVFormatContext *value) {
if (value) {
const auto deleter = IOPointer(value->pb);
avformat_close_input(&value);
}
}
CodecPointer MakeCodecPointer(not_null<AVStream*> stream) { CodecPointer MakeCodecPointer(not_null<AVStream*> stream) {
auto error = AvErrorWrap(); auto error = AvErrorWrap();
@ -120,10 +197,10 @@ void FrameDeleter::operator()(AVFrame *value) {
av_frame_free(&value); av_frame_free(&value);
} }
SwsContextPointer MakeSwsContextPointer( SwscalePointer MakeSwscalePointer(
not_null<AVFrame*> frame, not_null<AVFrame*> frame,
QSize resize, QSize resize,
SwsContextPointer *existing) { SwscalePointer *existing) {
// We have to use custom caching for SwsContext, because // We have to use custom caching for SwsContext, because
// sws_getCachedContext checks passed flags with existing context flags, // sws_getCachedContext checks passed flags with existing context flags,
// and re-creates context if they're different, but in the process of // and re-creates context if they're different, but in the process of
@ -139,7 +216,7 @@ SwsContextPointer MakeSwsContextPointer(
} }
if (frame->format <= AV_PIX_FMT_NONE || frame->format >= AV_PIX_FMT_NB) { if (frame->format <= AV_PIX_FMT_NONE || frame->format >= AV_PIX_FMT_NB) {
LogError(qstr("frame->format")); LogError(qstr("frame->format"));
return SwsContextPointer(); return SwscalePointer();
} }
const auto result = sws_getCachedContext( const auto result = sws_getCachedContext(
@ -157,12 +234,12 @@ SwsContextPointer MakeSwsContextPointer(
if (!result) { if (!result) {
LogError(qstr("sws_getCachedContext")); LogError(qstr("sws_getCachedContext"));
} }
return SwsContextPointer( return SwscalePointer(
result, result,
{ resize, QSize{ frame->width, frame->height }, frame->format }); { resize, QSize{ frame->width, frame->height }, frame->format });
} }
void SwsContextDeleter::operator()(SwsContext *value) { void SwscaleDeleter::operator()(SwsContext *value) {
if (value) { if (value) {
sws_freeContext(value); sws_freeContext(value);
} }
@ -341,11 +418,11 @@ QImage ConvertFrame(
from += deltaFrom; from += deltaFrom;
} }
} else { } else {
stream.swsContext = MakeSwsContextPointer( stream.swscale = MakeSwscalePointer(
frame, frame,
resize, resize,
&stream.swsContext); &stream.swscale);
if (!stream.swsContext) { if (!stream.swscale) {
return QImage(); return QImage();
} }
@ -354,7 +431,7 @@ QImage ConvertFrame(
int linesize[AV_NUM_DATA_POINTERS] = { storage.bytesPerLine(), 0 }; int linesize[AV_NUM_DATA_POINTERS] = { storage.bytesPerLine(), 0 };
const auto lines = sws_scale( const auto lines = sws_scale(
stream.swsContext.get(), stream.swscale.get(),
frame->data, frame->data,
frame->linesize, frame->linesize,
0, 0,

View File

@ -115,6 +115,26 @@ private:
}; };
struct IODeleter {
void operator()(AVIOContext *value);
};
using IOPointer = std::unique_ptr<AVIOContext, IODeleter>;
[[nodiscard]] IOPointer MakeIOPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
int64_t(*seek)(void *opaque, int64_t offset, int whence));
struct FormatDeleter {
void operator()(AVFormatContext *value);
};
using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
[[nodiscard]] FormatPointer MakeFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
int64_t(*seek)(void *opaque, int64_t offset, int whence));
struct CodecDeleter { struct CodecDeleter {
void operator()(AVCodecContext *value); void operator()(AVCodecContext *value);
}; };
@ -129,18 +149,18 @@ using FramePointer = std::unique_ptr<AVFrame, FrameDeleter>;
[[nodiscard]] bool FrameHasData(AVFrame *frame); [[nodiscard]] bool FrameHasData(AVFrame *frame);
void ClearFrameMemory(AVFrame *frame); void ClearFrameMemory(AVFrame *frame);
struct SwsContextDeleter { struct SwscaleDeleter {
QSize resize; QSize resize;
QSize frameSize; QSize frameSize;
int frameFormat = int(AV_PIX_FMT_NONE); int frameFormat = int(AV_PIX_FMT_NONE);
void operator()(SwsContext *value); void operator()(SwsContext *value);
}; };
using SwsContextPointer = std::unique_ptr<SwsContext, SwsContextDeleter>; using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
[[nodiscard]] SwsContextPointer MakeSwsContextPointer( [[nodiscard]] SwscalePointer MakeSwscalePointer(
not_null<AVFrame*> frame, not_null<AVFrame*> frame,
QSize resize, QSize resize,
SwsContextPointer *existing = nullptr); SwscalePointer *existing = nullptr);
struct Stream { struct Stream {
int index = -1; int index = -1;
@ -156,7 +176,7 @@ struct Stream {
// Video only. // Video only.
int rotation = 0; int rotation = 0;
SwsContextPointer swsContext; SwscalePointer swscale;
}; };
void LogError(QLatin1String method); void LogError(QLatin1String method);

View File

@ -1925,9 +1925,12 @@ void OverlayWidget::streamingReady(Streaming::Information &&info) {
validateStreamedGoodThumbnail(); validateStreamedGoodThumbnail();
if (videoShown()) { if (videoShown()) {
const auto contentSize = ConvertScale(videoSize()); const auto contentSize = ConvertScale(videoSize());
_w = contentSize.width(); if (_w != contentSize.width() || _h != contentSize.height()) {
_h = contentSize.height(); update(contentRect());
contentSizeChanged(); _w = contentSize.width();
_h = contentSize.height();
contentSizeChanged();
}
} }
this->update(contentRect()); this->update(contentRect());
playbackWaitingChange(false); playbackWaitingChange(false);
@ -2014,6 +2017,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
void OverlayWidget::handleStreamingError(Streaming::Error &&error) { void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
playbackWaitingChange(false); playbackWaitingChange(false);
updatePlaybackState();
} }
void OverlayWidget::playbackWaitingChange(bool waiting) { void OverlayWidget::playbackWaitingChange(bool waiting) {
@ -2124,7 +2128,13 @@ void OverlayWidget::refreshClipControllerGeometry() {
} }
void OverlayWidget::playbackControlsPlay() { void OverlayWidget::playbackControlsPlay() {
playbackPauseResume(); const auto legacy = _streamed->player.prepareLegacyState();
if (legacy.state == Player::State::StoppedAtStart) {
Data::HandleUnsupportedMedia(_doc, _msgid);
close();
} else {
playbackPauseResume();
}
} }
void OverlayWidget::playbackControlsPause() { void OverlayWidget::playbackControlsPause() {