998 lines
25 KiB
C++
998 lines
25 KiB
C++
/*
|
|
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/streaming/media_streaming_audio_track.h"
|
|
#include "media/streaming/media_streaming_video_track.h"
|
|
#include "media/audio/media_audio.h" // for SupportsSpeedControl()
|
|
#include "data/data_document.h" // for DocumentData::duration()
|
|
|
|
namespace Media {
|
|
namespace Streaming {
|
|
namespace {
|
|
|
|
constexpr auto kBufferFor = 3 * crl::time(1000);
|
|
constexpr auto kLoadInAdvanceForRemote = 32 * crl::time(1000);
|
|
constexpr auto kLoadInAdvanceForLocal = 5 * crl::time(1000);
|
|
constexpr auto kMsFrequency = 1000; // 1000 ms per second.
|
|
|
|
// If we played for 3 seconds and got stuck it looks like we're loading
|
|
// slower than we're playing, so load full file in that case.
|
|
//constexpr auto kLoadFullIfStuckAfterPlayback = 3 * crl::time(1000);
|
|
|
|
[[nodiscard]] bool FullTrackReceived(const TrackState &state) {
|
|
return (state.duration != kTimeUnknown)
|
|
&& (state.receivedTill == state.duration);
|
|
}
|
|
|
|
void SaveValidStateInformation(TrackState &to, TrackState &&from) {
|
|
Expects(from.position != kTimeUnknown);
|
|
Expects(from.receivedTill != kTimeUnknown);
|
|
Expects(from.duration != kTimeUnknown);
|
|
|
|
to.duration = from.duration;
|
|
to.position = from.position;
|
|
to.receivedTill = (to.receivedTill == kTimeUnknown)
|
|
? from.receivedTill
|
|
: std::clamp(
|
|
std::max(from.receivedTill, to.receivedTill),
|
|
to.position,
|
|
to.duration);
|
|
}
|
|
|
|
void SaveValidAudioInformation(
|
|
AudioInformation &to,
|
|
AudioInformation &&from) {
|
|
SaveValidStateInformation(to.state, std::move(from.state));
|
|
}
|
|
|
|
void SaveValidVideoInformation(
|
|
VideoInformation &to,
|
|
VideoInformation &&from) {
|
|
Expects(!from.size.isEmpty());
|
|
Expects(!from.cover.isNull());
|
|
|
|
SaveValidStateInformation(to.state, std::move(from.state));
|
|
to.size = from.size;
|
|
to.cover = std::move(from.cover);
|
|
to.rotation = from.rotation;
|
|
}
|
|
|
|
void SaveValidStartInformation(Information &to, Information &&from) {
|
|
if (from.audio.state.duration != kTimeUnknown) {
|
|
SaveValidAudioInformation(to.audio, std::move(from.audio));
|
|
}
|
|
if (from.video.state.duration != kTimeUnknown) {
|
|
SaveValidVideoInformation(to.video, std::move(from.video));
|
|
}
|
|
if (from.headerSize && !to.headerSize) {
|
|
to.headerSize = from.headerSize;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Player::Player(std::shared_ptr<Reader> reader)
|
|
: _file(std::make_unique<File>(std::move(reader)))
|
|
, _remoteLoader(_file->isRemoteLoader())
|
|
, _renderFrameTimer([=] { checkNextFrameRender(); }) {
|
|
}
|
|
|
|
not_null<FileDelegate*> Player::delegate() {
|
|
return static_cast<FileDelegate*>(this);
|
|
}
|
|
|
|
void Player::checkNextFrameRender() {
|
|
Expects(_nextFrameTime != kTimeUnknown);
|
|
|
|
const auto now = crl::now();
|
|
if (now < _nextFrameTime) {
|
|
if (!_renderFrameTimer.isActive()) {
|
|
_renderFrameTimer.callOnce(_nextFrameTime - now);
|
|
}
|
|
} else {
|
|
_renderFrameTimer.cancel();
|
|
renderFrame(now);
|
|
}
|
|
}
|
|
|
|
void Player::checkNextFrameAvailability() {
|
|
Expects(_video != nullptr);
|
|
|
|
_nextFrameTime = _video->nextFrameDisplayTime();
|
|
Assert(_nextFrameTime != kFrameDisplayTimeAlreadyDone);
|
|
if (_nextFrameTime != kTimeUnknown) {
|
|
checkNextFrameRender();
|
|
}
|
|
}
|
|
|
|
void Player::renderFrame(crl::time now) {
|
|
Expects(_video != nullptr);
|
|
Expects(_nextFrameTime != kTimeUnknown);
|
|
Expects(_nextFrameTime != kFrameDisplayTimeAlreadyDone);
|
|
|
|
const auto position = _video->markFrameDisplayed(now);
|
|
if (_options.waitForMarkAsShown) {
|
|
_currentFrameTime = _nextFrameTime;
|
|
_nextFrameTime = kFrameDisplayTimeAlreadyDone;
|
|
} else {
|
|
_video->markFrameShown();
|
|
_nextFrameTime = kTimeUnknown;
|
|
}
|
|
|
|
Assert(position != kTimeUnknown);
|
|
videoPlayedTill(position);
|
|
}
|
|
|
|
bool Player::markFrameShown() {
|
|
Expects(_video != nullptr);
|
|
|
|
if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) {
|
|
_nextFrameTime = kTimeUnknown;
|
|
_video->addTimelineDelay(crl::now() - _currentFrameTime);
|
|
}
|
|
return _video->markFrameShown();
|
|
}
|
|
|
|
void Player::setLoaderPriority(int priority) {
|
|
_file->setLoaderPriority(priority);
|
|
}
|
|
|
|
template <typename Track>
|
|
void Player::trackReceivedTill(
|
|
const Track &track,
|
|
TrackState &state,
|
|
crl::time position) {
|
|
if (position == kTimeUnknown) {
|
|
return;
|
|
} else if (state.duration != kTimeUnknown) {
|
|
if (state.receivedTill < position) {
|
|
state.receivedTill = position;
|
|
trackSendReceivedTill(track, state);
|
|
}
|
|
} else {
|
|
state.receivedTill = position;
|
|
}
|
|
if (!_pauseReading
|
|
&& bothReceivedEnough(loadInAdvanceFor())
|
|
&& !receivedTillEnd()) {
|
|
_pauseReading = true;
|
|
}
|
|
}
|
|
|
|
template <typename Track>
|
|
void Player::trackPlayedTill(
|
|
const Track &track,
|
|
TrackState &state,
|
|
crl::time position) {
|
|
const auto guard = base::make_weak(&_sessionGuard);
|
|
trackReceivedTill(track, state, position);
|
|
if (guard && position != kTimeUnknown) {
|
|
state.position = position;
|
|
const auto value = _options.loop
|
|
? (position % computeTotalDuration())
|
|
: position;
|
|
_updates.fire({ PlaybackUpdate<Track>{ value } });
|
|
}
|
|
if (_pauseReading
|
|
&& (!bothReceivedEnough(loadInAdvanceFor()) || receivedTillEnd())) {
|
|
_pauseReading = false;
|
|
_file->wake();
|
|
}
|
|
}
|
|
|
|
template <typename Track>
|
|
void Player::trackSendReceivedTill(
|
|
const Track &track,
|
|
TrackState &state) {
|
|
Expects(state.duration != kTimeUnknown);
|
|
Expects(state.receivedTill != kTimeUnknown);
|
|
|
|
if (!_remoteLoader || _fullInCacheSinceStart.value_or(false)) {
|
|
return;
|
|
}
|
|
const auto receivedTill = std::max(
|
|
state.receivedTill,
|
|
_previousReceivedTill);
|
|
const auto value = _options.loop
|
|
? (receivedTill % computeTotalDuration())
|
|
: receivedTill;
|
|
_updates.fire({ PreloadedUpdate<Track>{ value } });
|
|
}
|
|
|
|
void Player::audioReceivedTill(crl::time position) {
|
|
Expects(_audio != nullptr);
|
|
|
|
trackReceivedTill(*_audio, _information.audio.state, position);
|
|
checkResumeFromWaitingForData();
|
|
}
|
|
|
|
void Player::audioPlayedTill(crl::time position) {
|
|
Expects(_audio != nullptr);
|
|
|
|
trackPlayedTill(*_audio, _information.audio.state, position);
|
|
}
|
|
|
|
void Player::videoReceivedTill(crl::time position) {
|
|
Expects(_video != nullptr);
|
|
|
|
trackReceivedTill(*_video, _information.video.state, position);
|
|
checkResumeFromWaitingForData();
|
|
}
|
|
|
|
void Player::videoPlayedTill(crl::time position) {
|
|
Expects(_video != nullptr);
|
|
|
|
trackPlayedTill(*_video, _information.video.state, position);
|
|
}
|
|
|
|
bool Player::fileReady(int headerSize, Stream &&video, Stream &&audio) {
|
|
_waitingForData = false;
|
|
|
|
const auto weak = base::make_weak(&_sessionGuard);
|
|
const auto ready = [=](const Information &data) {
|
|
crl::on_main(weak, [=, data = data]() mutable {
|
|
data.headerSize = headerSize;
|
|
streamReady(std::move(data));
|
|
});
|
|
};
|
|
const auto error = [=](Error error) {
|
|
crl::on_main(weak, [=] {
|
|
streamFailed(error);
|
|
});
|
|
};
|
|
const auto mode = _options.mode;
|
|
if ((mode != Mode::Audio && mode != Mode::Both)
|
|
|| audio.duration == kDurationUnavailable) {
|
|
audio = Stream();
|
|
}
|
|
if (mode != Mode::Video && mode != Mode::Both) {
|
|
video = Stream();
|
|
}
|
|
if (audio.duration == kDurationUnavailable) {
|
|
LOG(("Streaming Error: Audio stream with unknown duration."));
|
|
return false;
|
|
} else if (audio.codec) {
|
|
if (_options.audioId.audio() != nullptr) {
|
|
_audioId = AudioMsgId(
|
|
_options.audioId.audio(),
|
|
_options.audioId.contextId(),
|
|
AudioMsgId::CreateExternalPlayId());
|
|
} else {
|
|
_audioId = AudioMsgId::ForVideo();
|
|
}
|
|
_audio = std::make_unique<AudioTrack>(
|
|
_options,
|
|
std::move(audio),
|
|
_audioId,
|
|
ready,
|
|
error);
|
|
} else if (audio.index >= 0) {
|
|
LOG(("Streaming Error: No codec for audio stream %1, mode %2."
|
|
).arg(audio.index
|
|
).arg(int(mode)));
|
|
return false;
|
|
} else {
|
|
_audioId = AudioMsgId();
|
|
}
|
|
if (video.codec) {
|
|
_video = std::make_unique<VideoTrack>(
|
|
_options,
|
|
std::move(video),
|
|
_audioId,
|
|
ready,
|
|
error);
|
|
} else if (video.index >= 0) {
|
|
LOG(("Streaming Error: No codec for video stream %1, mode %2."
|
|
).arg(video.index
|
|
).arg(int(mode)));
|
|
return false;
|
|
}
|
|
if ((mode == Mode::Audio && !_audio)
|
|
|| (mode == Mode::Video && !_video)
|
|
|| (!_audio && !_video)) {
|
|
LOG(("Streaming Error: Required stream not found for mode %1."
|
|
).arg(int(mode)));
|
|
return false;
|
|
} else if (_audio
|
|
&& _video
|
|
&& _video->streamDuration() == kDurationUnavailable) {
|
|
LOG(("Streaming Error: Both streams with unknown video duration."));
|
|
return false;
|
|
}
|
|
_totalDuration = std::max(
|
|
_audio ? _audio->streamDuration() : kTimeUnknown,
|
|
_video ? _video->streamDuration() : kTimeUnknown);
|
|
|
|
Ensures(_totalDuration > 1);
|
|
return true;
|
|
}
|
|
|
|
void Player::fileError(Error error) {
|
|
_waitingForData = false;
|
|
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
fail(error);
|
|
});
|
|
}
|
|
|
|
void Player::fileFullInCache(bool fullInCache) {
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
if (!_fullInCacheSinceStart.has_value()) {
|
|
_fullInCacheSinceStart = fullInCache;
|
|
}
|
|
_fullInCache.fire_copy(fullInCache);
|
|
});
|
|
}
|
|
|
|
void Player::fileWaitingForData() {
|
|
if (_waitingForData) {
|
|
return;
|
|
}
|
|
_waitingForData = true;
|
|
if (_audio) {
|
|
_audio->waitForData();
|
|
}
|
|
if (_video) {
|
|
_video->waitForData();
|
|
}
|
|
}
|
|
|
|
bool Player::fileProcessPackets(
|
|
base::flat_map<int, std::vector<FFmpeg::Packet>> &packets) {
|
|
_waitingForData = false;
|
|
for (auto &[index, list] : packets) {
|
|
if (list.empty()) {
|
|
continue;
|
|
}
|
|
if (_audio && _audio->streamIndex() == index) {
|
|
//for (const auto &packet : list) {
|
|
// // Maybe it is enough to count by list.back()?.. hope so.
|
|
// accumulate_max(
|
|
// _durationByLastAudioPacket,
|
|
// durationByPacket(*_audio, packet));
|
|
//}
|
|
accumulate_max(
|
|
_durationByLastAudioPacket,
|
|
durationByPacket(*_audio, list.back()));
|
|
const auto till = _loopingShift + std::clamp(
|
|
FFmpeg::PacketPosition(
|
|
list.back(),
|
|
_audio->streamTimeBase()),
|
|
crl::time(0),
|
|
computeAudioDuration() - 1);
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
audioReceivedTill(till);
|
|
});
|
|
_audio->process(base::take(list));
|
|
} else if (_video && _video->streamIndex() == index) {
|
|
//for (const auto &packet : list) {
|
|
// // Maybe it is enough to count by list.back()?.. hope so.
|
|
// accumulate_max(
|
|
// _durationByLastVideoPacket,
|
|
// durationByPacket(*_video, packet));
|
|
//}
|
|
accumulate_max(
|
|
_durationByLastVideoPacket,
|
|
durationByPacket(*_video, list.back()));
|
|
const auto till = _loopingShift + std::clamp(
|
|
FFmpeg::PacketPosition(
|
|
list.back(),
|
|
_video->streamTimeBase()),
|
|
crl::time(0),
|
|
computeVideoDuration() - 1);
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
videoReceivedTill(till);
|
|
});
|
|
_video->process(base::take(list));
|
|
} else {
|
|
list.clear(); // Free non-needed packets.
|
|
}
|
|
}
|
|
return fileReadMore();
|
|
}
|
|
|
|
void Player::fileProcessEndOfFile() {
|
|
_waitingForData = false;
|
|
_readTillEnd = true;
|
|
setDurationByPackets();
|
|
const auto generateEmptyQueue = [] {
|
|
auto result = std::vector<FFmpeg::Packet>();
|
|
result.emplace_back();
|
|
return result;
|
|
};
|
|
if (_audio) {
|
|
const auto till = _loopingShift + computeAudioDuration();
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
audioReceivedTill(till);
|
|
});
|
|
_audio->process(generateEmptyQueue());
|
|
}
|
|
if (_video) {
|
|
const auto till = _loopingShift + computeVideoDuration();
|
|
crl::on_main(&_sessionGuard, [=] {
|
|
videoReceivedTill(till);
|
|
});
|
|
_video->process(generateEmptyQueue());
|
|
}
|
|
}
|
|
|
|
bool Player::fileReadMore() {
|
|
if (_options.loop && _readTillEnd) {
|
|
const auto duration = computeTotalDuration();
|
|
if (duration == kDurationUnavailable) {
|
|
LOG(("Streaming Error: "
|
|
"Couldn't find out the real stream duration."));
|
|
fileError(Error::InvalidData);
|
|
return false;
|
|
}
|
|
_loopingShift += duration;
|
|
_readTillEnd = false;
|
|
return true;
|
|
}
|
|
return !_readTillEnd && !_pauseReading;
|
|
}
|
|
|
|
void Player::streamReady(Information &&information) {
|
|
SaveValidStartInformation(_information, std::move(information));
|
|
provideStartInformation();
|
|
}
|
|
|
|
void Player::streamFailed(Error error) {
|
|
fail(error);
|
|
}
|
|
|
|
template <typename Track>
|
|
int Player::durationByPacket(
|
|
const Track &track,
|
|
const FFmpeg::Packet &packet) {
|
|
// We've set this value on the first cycle.
|
|
if (_loopingShift || _totalDuration != kDurationUnavailable) {
|
|
return 0;
|
|
}
|
|
const auto result = DurationByPacket(packet, track.streamTimeBase());
|
|
if (result < 0) {
|
|
fileError(Error::InvalidData);
|
|
return 0;
|
|
}
|
|
|
|
Ensures(result > 0);
|
|
return result;
|
|
}
|
|
|
|
void Player::setDurationByPackets() {
|
|
if (_loopingShift || _totalDuration != kDurationUnavailable) {
|
|
return;
|
|
}
|
|
const auto duration = std::max(
|
|
_durationByLastAudioPacket,
|
|
_durationByLastVideoPacket);
|
|
if (duration > 1) {
|
|
_durationByPackets = duration;
|
|
} else {
|
|
LOG(("Streaming Error: Bad total duration by packets: %1"
|
|
).arg(duration));
|
|
fileError(Error::InvalidData);
|
|
}
|
|
}
|
|
|
|
void Player::provideStartInformation() {
|
|
Expects(_stage == Stage::Initializing);
|
|
|
|
if ((_audio && _information.audio.state.duration == kTimeUnknown)
|
|
|| (_video && _information.video.state.duration == kTimeUnknown)) {
|
|
return; // Not ready yet.
|
|
} else if ((!_audio && !_video)
|
|
|| (!_audio && _options.mode == Mode::Audio)
|
|
|| (!_video && _options.mode == Mode::Video)) {
|
|
fail(Error::OpenFailed);
|
|
} else {
|
|
_stage = Stage::Ready;
|
|
|
|
if (_audio && _audioFinished) {
|
|
// Audio was stopped before it was ready.
|
|
_audio->stop();
|
|
}
|
|
|
|
// Don't keep the reference to the video cover.
|
|
auto copy = _information;
|
|
_information.video.cover = QImage();
|
|
|
|
_updates.fire(Update{ std::move(copy) });
|
|
|
|
if (_stage == Stage::Ready && !_paused) {
|
|
_paused = true;
|
|
updatePausedState();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::fail(Error error) {
|
|
_sessionLifetime = rpl::lifetime();
|
|
const auto stopGuarded = crl::guard(&_sessionGuard, [=] { stop(); });
|
|
_lastFailure = error;
|
|
_updates.fire_error(std::move(error));
|
|
stopGuarded();
|
|
}
|
|
|
|
void Player::play(const PlaybackOptions &options) {
|
|
Expects(options.speed >= 0.5 && options.speed <= 2.);
|
|
|
|
// Looping video with audio is not supported for now.
|
|
Expects(!options.loop || (options.mode != Mode::Both));
|
|
|
|
const auto previous = getCurrentReceivedTill(computeTotalDuration());
|
|
|
|
stop(true);
|
|
_lastFailure = std::nullopt;
|
|
|
|
savePreviousReceivedTill(options, previous);
|
|
_options = options;
|
|
if (!Media::Audio::SupportsSpeedControl()) {
|
|
_options.speed = 1.;
|
|
}
|
|
_stage = Stage::Initializing;
|
|
_file->start(delegate(), _options.position);
|
|
}
|
|
|
|
void Player::savePreviousReceivedTill(
|
|
const PlaybackOptions &options,
|
|
crl::time previousReceivedTill) {
|
|
// Save previous 'receivedTill' values if we seek inside the range.
|
|
_previousReceivedTill = ((options.position >= _options.position)
|
|
&& (options.mode == _options.mode)
|
|
&& (options.position < previousReceivedTill))
|
|
? previousReceivedTill
|
|
: kTimeUnknown;
|
|
}
|
|
|
|
crl::time Player::loadInAdvanceFor() const {
|
|
return _remoteLoader ? kLoadInAdvanceForRemote : kLoadInAdvanceForLocal;
|
|
}
|
|
|
|
crl::time Player::computeTotalDuration() const {
|
|
if (_totalDuration != kDurationUnavailable) {
|
|
return _totalDuration;
|
|
} else if (const auto byPackets = _durationByPackets.load()) {
|
|
return byPackets;
|
|
}
|
|
return kDurationUnavailable;
|
|
}
|
|
|
|
crl::time Player::computeAudioDuration() const {
|
|
Expects(_audio != nullptr);
|
|
|
|
const auto result = _audio->streamDuration();
|
|
if (result != kDurationUnavailable) {
|
|
return result;
|
|
} else if ((_loopingShift || _readTillEnd)
|
|
&& _durationByLastAudioPacket) {
|
|
// We looped, so it already holds full stream duration.
|
|
return _durationByLastAudioPacket;
|
|
}
|
|
return kDurationUnavailable;
|
|
}
|
|
|
|
crl::time Player::computeVideoDuration() const {
|
|
Expects(_video != nullptr);
|
|
|
|
const auto result = _video->streamDuration();
|
|
if (result != kDurationUnavailable) {
|
|
return result;
|
|
} else if ((_loopingShift || _readTillEnd)
|
|
&& _durationByLastVideoPacket) {
|
|
// We looped, so it already holds full stream duration.
|
|
return _durationByLastVideoPacket;
|
|
}
|
|
return kDurationUnavailable;
|
|
}
|
|
|
|
void Player::pause() {
|
|
Expects(active());
|
|
|
|
_pausedByUser = true;
|
|
updatePausedState();
|
|
}
|
|
|
|
void Player::resume() {
|
|
Expects(active());
|
|
|
|
_pausedByUser = false;
|
|
updatePausedState();
|
|
}
|
|
|
|
void Player::stop() {
|
|
stop(false);
|
|
}
|
|
|
|
void Player::stopAudio() {
|
|
if (!_video) {
|
|
stop();
|
|
} else if (_audio) {
|
|
_audioFinished = true;
|
|
if (_information.audio.state.duration != kTimeUnknown) {
|
|
// Audio is ready.
|
|
_audio->stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::updatePausedState() {
|
|
const auto paused = _pausedByUser || _pausedByWaitingForData;
|
|
if (_paused == paused) {
|
|
return;
|
|
}
|
|
_paused = paused;
|
|
if (!_paused && _stage == Stage::Ready) {
|
|
const auto guard = base::make_weak(&_sessionGuard);
|
|
start();
|
|
if (!guard) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_stage != Stage::Started) {
|
|
return;
|
|
}
|
|
if (_paused) {
|
|
_pausedTime = crl::now();
|
|
//if (_pausedByWaitingForData
|
|
// && _pausedTime - _startedTime > kLoadFullIfStuckAfterPlayback) {
|
|
// _loadFull = true;
|
|
//}
|
|
if (_audio) {
|
|
_audio->pause(_pausedTime);
|
|
}
|
|
if (_video) {
|
|
_video->pause(_pausedTime);
|
|
}
|
|
} else {
|
|
_startedTime = crl::now();
|
|
if (_audio) {
|
|
_audio->resume(_startedTime);
|
|
}
|
|
if (_video) {
|
|
_video->resume(_startedTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Player::trackReceivedEnough(
|
|
const TrackState &state,
|
|
crl::time amount) const {
|
|
return (!_options.loop && FullTrackReceived(state))
|
|
|| (state.position != kTimeUnknown
|
|
&& (state.position + std::min(amount, state.duration)
|
|
<= state.receivedTill));
|
|
}
|
|
|
|
bool Player::bothReceivedEnough(crl::time amount) const {
|
|
const auto &info = _information;
|
|
return (!_audio || trackReceivedEnough(info.audio.state, amount))
|
|
&& (!_video || trackReceivedEnough(info.video.state, amount));
|
|
}
|
|
|
|
bool Player::receivedTillEnd() const {
|
|
if (_options.loop) {
|
|
return false;
|
|
}
|
|
return (!_video || FullTrackReceived(_information.video.state))
|
|
&& (!_audio || FullTrackReceived(_information.audio.state));
|
|
}
|
|
|
|
void Player::checkResumeFromWaitingForData() {
|
|
if (_pausedByWaitingForData && bothReceivedEnough(kBufferFor)) {
|
|
_pausedByWaitingForData = false;
|
|
updatePausedState();
|
|
_updates.fire({ WaitingForData{ false } });
|
|
}
|
|
}
|
|
|
|
void Player::start() {
|
|
Expects(_stage == Stage::Ready);
|
|
|
|
_stage = Stage::Started;
|
|
const auto guard = base::make_weak(&_sessionGuard);
|
|
|
|
rpl::merge(
|
|
_audio ? _audio->waitingForData() : nullptr,
|
|
_video ? _video->waitingForData() : nullptr
|
|
) | rpl::filter([=] {
|
|
return !bothReceivedEnough(kBufferFor);
|
|
}) | rpl::start_with_next([=] {
|
|
_pausedByWaitingForData = true;
|
|
updatePausedState();
|
|
_updates.fire({ WaitingForData{ true } });
|
|
}, _sessionLifetime);
|
|
|
|
if (guard && _audio && !_audioFinished) {
|
|
_audio->playPosition(
|
|
) | rpl::start_with_next_done([=](crl::time position) {
|
|
audioPlayedTill(position);
|
|
}, [=] {
|
|
Expects(_stage == Stage::Started);
|
|
|
|
_audioFinished = true;
|
|
if (!_video || _videoFinished) {
|
|
_updates.fire({ Finished() });
|
|
}
|
|
}, _sessionLifetime);
|
|
}
|
|
|
|
if (guard && _video) {
|
|
_video->checkNextFrame(
|
|
) | rpl::start_with_next_done([=] {
|
|
checkVideoStep();
|
|
}, [=] {
|
|
Assert(_stage == Stage::Started);
|
|
|
|
_videoFinished = true;
|
|
if (!_audio || _audioFinished) {
|
|
_updates.fire({ Finished() });
|
|
}
|
|
}, _sessionLifetime);
|
|
|
|
crl::on_main_update_requests(
|
|
) | rpl::filter([=] {
|
|
return !_videoFinished;
|
|
}) | rpl::start_with_next([=] {
|
|
checkVideoStep();
|
|
}, _sessionLifetime);
|
|
}
|
|
if (guard && _audio) {
|
|
if (_audioFinished) {
|
|
if (!_video || _videoFinished) {
|
|
_updates.fire({ Finished() });
|
|
}
|
|
} else {
|
|
trackSendReceivedTill(*_audio, _information.audio.state);
|
|
}
|
|
}
|
|
if (guard && _video) {
|
|
trackSendReceivedTill(*_video, _information.video.state);
|
|
}
|
|
}
|
|
|
|
void Player::checkVideoStep() {
|
|
if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) {
|
|
return;
|
|
} else if (_nextFrameTime != kTimeUnknown) {
|
|
checkNextFrameRender();
|
|
} else {
|
|
checkNextFrameAvailability();
|
|
}
|
|
}
|
|
|
|
void Player::stop(bool stillActive) {
|
|
_file->stop(stillActive);
|
|
_sessionLifetime = rpl::lifetime();
|
|
_stage = Stage::Uninitialized;
|
|
_audio = nullptr;
|
|
_video = nullptr;
|
|
invalidate_weak_ptrs(&_sessionGuard);
|
|
_pausedByUser = _pausedByWaitingForData = _paused = false;
|
|
_renderFrameTimer.cancel();
|
|
_nextFrameTime = kTimeUnknown;
|
|
_audioFinished = false;
|
|
_videoFinished = false;
|
|
_pauseReading = false;
|
|
_readTillEnd = false;
|
|
_loopingShift = 0;
|
|
_durationByPackets = 0;
|
|
_durationByLastAudioPacket = 0;
|
|
_durationByLastVideoPacket = 0;
|
|
const auto header = _information.headerSize;
|
|
_information = Information();
|
|
_information.headerSize = header;
|
|
}
|
|
|
|
std::optional<Error> Player::failed() const {
|
|
return _lastFailure;
|
|
}
|
|
|
|
bool Player::playing() const {
|
|
return (_stage == Stage::Started)
|
|
&& !paused()
|
|
&& !finished()
|
|
&& !failed();
|
|
}
|
|
|
|
bool Player::buffering() const {
|
|
return _pausedByWaitingForData;
|
|
}
|
|
|
|
bool Player::paused() const {
|
|
return _pausedByUser && active();
|
|
}
|
|
|
|
bool Player::finished() const {
|
|
return (_stage == Stage::Started)
|
|
&& (!_audio || _audioFinished)
|
|
&& (!_video || _videoFinished);
|
|
}
|
|
|
|
float64 Player::speed() const {
|
|
return _options.speed;
|
|
}
|
|
|
|
void Player::setSpeed(float64 speed) {
|
|
Expects(speed >= 0.5 && speed <= 2.);
|
|
|
|
if (!Media::Audio::SupportsSpeedControl()) {
|
|
speed = 1.;
|
|
}
|
|
if (_options.speed != speed) {
|
|
_options.speed = speed;
|
|
if (active()) {
|
|
if (_audio) {
|
|
_audio->setSpeed(speed);
|
|
}
|
|
if (_video) {
|
|
_video->setSpeed(speed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::setWaitForMarkAsShown(bool wait) {
|
|
if (_options.waitForMarkAsShown != wait) {
|
|
_options.waitForMarkAsShown = wait;
|
|
if (_video) {
|
|
_video->setWaitForMarkAsShown(wait);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Player::active() const {
|
|
return (_stage != Stage::Uninitialized) && !finished() && !failed();
|
|
}
|
|
|
|
bool Player::ready() const {
|
|
return (_stage != Stage::Uninitialized)
|
|
&& (_stage != Stage::Initializing);
|
|
}
|
|
|
|
rpl::producer<Update, Error> Player::updates() const {
|
|
return _updates.events();
|
|
}
|
|
|
|
rpl::producer<bool> Player::fullInCache() const {
|
|
return _fullInCache.events();
|
|
}
|
|
|
|
QSize Player::videoSize() const {
|
|
return _information.video.size;
|
|
}
|
|
|
|
QImage Player::frame(
|
|
const FrameRequest &request,
|
|
const Instance *instance) const {
|
|
Expects(_video != nullptr);
|
|
|
|
return _video->frame(request, instance);
|
|
}
|
|
|
|
FrameWithInfo Player::frameWithInfo(const Instance *instance) const {
|
|
Expects(_video != nullptr);
|
|
|
|
return _video->frameWithInfo(instance);
|
|
}
|
|
|
|
QImage Player::currentFrameImage() const {
|
|
Expects(_video != nullptr);
|
|
|
|
return _video->currentFrameImage();
|
|
}
|
|
|
|
void Player::unregisterInstance(not_null<const Instance*> instance) {
|
|
if (_video) {
|
|
_video->unregisterInstance(instance);
|
|
}
|
|
}
|
|
|
|
Media::Player::TrackState Player::prepareLegacyState() const {
|
|
using namespace Media::Player;
|
|
|
|
auto result = Media::Player::TrackState();
|
|
result.id = _audioId.externalPlayId() ? _audioId : _options.audioId;
|
|
result.state = (_lastFailure == Error::OpenFailed
|
|
|| _lastFailure == Error::NotStreamable)
|
|
? State::StoppedAtStart
|
|
: _lastFailure
|
|
? State::StoppedAtError
|
|
: finished()
|
|
? State::StoppedAtEnd
|
|
: (_stage == Stage::Uninitialized)
|
|
? State::Stopped
|
|
: paused()
|
|
? State::Paused
|
|
: State::Playing;
|
|
result.position = std::max(
|
|
_information.audio.state.position,
|
|
_information.video.state.position);
|
|
result.length = computeTotalDuration();
|
|
if (result.position == kTimeUnknown) {
|
|
result.position = _options.position;
|
|
} else if (_options.loop && result.length > 0) {
|
|
result.position %= result.length;
|
|
}
|
|
result.receivedTill = (_remoteLoader
|
|
&& !_fullInCacheSinceStart.value_or(false))
|
|
? getCurrentReceivedTill(result.length)
|
|
: 0;
|
|
result.frequency = kMsFrequency;
|
|
result.fileHeaderSize = _information.headerSize;
|
|
|
|
if (result.length == kTimeUnknown) {
|
|
const auto document = _options.audioId.audio();
|
|
const auto duration = document ? document->getDuration() : 0;
|
|
if (duration > 0) {
|
|
result.length = duration * crl::time(1000);
|
|
} else {
|
|
result.length = std::max(crl::time(result.position), crl::time(0));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
crl::time Player::getCurrentReceivedTill(crl::time duration) const {
|
|
const auto forTrack = [&](const TrackState &state) {
|
|
return (state.duration > 0 && state.receivedTill == state.duration)
|
|
? std::max(state.receivedTill, duration)
|
|
: state.receivedTill;
|
|
};
|
|
const auto previous = std::max(_previousReceivedTill, crl::time(0));
|
|
const auto result = std::min(
|
|
std::max(forTrack(_information.audio.state), previous),
|
|
std::max(forTrack(_information.video.state), previous));
|
|
return (result >= 0 && duration > 1 && _options.loop)
|
|
? (result % duration)
|
|
: result;
|
|
}
|
|
|
|
void Player::lock() {
|
|
++_locks;
|
|
}
|
|
|
|
void Player::unlock() {
|
|
Expects(_locks > 0);
|
|
|
|
--_locks;
|
|
if (!_locks) {
|
|
stopAudio();
|
|
if (active()) {
|
|
setSpeed(1.);
|
|
}
|
|
setWaitForMarkAsShown(true);
|
|
}
|
|
}
|
|
|
|
bool Player::locked() const {
|
|
return (_locks > 0);
|
|
}
|
|
|
|
rpl::lifetime &Player::lifetime() {
|
|
return _lifetime;
|
|
}
|
|
|
|
Player::~Player() {
|
|
// The order of field destruction is important.
|
|
//
|
|
// We are forced to maintain the correct order in the stop() method,
|
|
// because it can be called even before the player destruction.
|
|
//
|
|
// So instead of maintaining it in the class definition as well we
|
|
// simply call stop() here, after that the destruction is trivial.
|
|
stop();
|
|
}
|
|
|
|
} // namespace Streaming
|
|
} // namespace Media
|