Implement precise seek in streaming.

This commit is contained in:
John Preston 2019-02-22 16:39:32 +04:00
parent 44c562d8ba
commit 3e9b811875
6 changed files with 70 additions and 46 deletions

View File

@ -302,6 +302,7 @@ void StartStreaming(
static auto options = Media::Streaming::PlaybackOptions(); static auto options = Media::Streaming::PlaybackOptions();
static auto speed = 1.; static auto speed = 1.;
static auto step = pow(2., 1. / 12); static auto step = pow(2., 1. / 12);
static auto frame = QImage();
class Panel class Panel
#if defined Q_OS_MAC && !defined OS_MAC_OLD #if defined Q_OS_MAC && !defined OS_MAC_OLD
@ -339,6 +340,9 @@ void StartStreaming(
player->pause(); player->pause();
} }
void mouseReleaseEvent(QMouseEvent *e) override { void mouseReleaseEvent(QMouseEvent *e) override {
if (player->ready()) {
frame = player->frame({});
}
preloaded = position = options.position = std::clamp( preloaded = position = options.position = std::clamp(
(duration * e->pos().x()) / width(), (duration * e->pos().x()) / width(),
crl::time(0), crl::time(0),
@ -364,6 +368,7 @@ void StartStreaming(
options.speed = speed; options.speed = speed;
//options.syncVideoByAudio = false; //options.syncVideoByAudio = false;
preloaded = position = options.position = 0; preloaded = position = options.position = 0;
frame = QImage();
player->play(options); player->play(options);
player->updates( player->updates(
) | rpl::start_with_next_error_done([=](Update &&update) { ) | rpl::start_with_next_error_done([=](Update &&update) {
@ -391,7 +396,11 @@ void StartStreaming(
if (player->ready()) { if (player->ready()) {
Painter(video.get()).drawImage( Painter(video.get()).drawImage(
video->rect(), video->rect(),
player->frame(FrameRequest())); player->frame({}));
} else if (!frame.isNull()) {
Painter(video.get()).drawImage(
video->rect(),
frame);
} else { } else {
Painter(video.get()).fillRect( Painter(video.get()).fillRect(
rect, rect,

View File

@ -61,29 +61,51 @@ bool AudioTrack::initialized() const {
} }
bool AudioTrack::tryReadFirstFrame(Packet &&packet) { bool AudioTrack::tryReadFirstFrame(Packet &&packet) {
// #TODO streaming fix seek to the end.
if (ProcessPacket(_stream, std::move(packet)).failed()) { if (ProcessPacket(_stream, std::move(packet)).failed()) {
return false; return false;
} }
if (const auto error = ReadNextFrame(_stream)) { if (const auto error = ReadNextFrame(_stream)) {
if (error.code() == AVERROR_EOF) { if (error.code() == AVERROR_EOF) {
// #TODO streaming fix seek to the end. if (!_initialSkippingFrame) {
return false; return false;
}
// Return the last valid frame if we seek too far.
_stream.frame = std::move(_initialSkippingFrame);
return processFirstFrame();
} else if (error.code() != AVERROR(EAGAIN) || _noMoreData) { } else if (error.code() != AVERROR(EAGAIN) || _noMoreData) {
return false; return false;
} else {
// Waiting for more packets.
return true;
} }
return true;
} else if (!fillStateFromFrame()) { } else if (!fillStateFromFrame()) {
return false; return false;
} else if (_startedPosition < _options.position) {
// Seek was with AVSEEK_FLAG_BACKWARD so first we get old frames.
// Try skipping frames until one is after the requested position.
std::swap(_initialSkippingFrame, _stream.frame);
if (!_stream.frame) {
_stream.frame = MakeFramePointer();
}
return true;
} else {
return processFirstFrame();
} }
}
bool AudioTrack::processFirstFrame() {
mixerInit(); mixerInit();
callReady(); callReady();
return true; return true;
} }
bool AudioTrack::fillStateFromFrame() { bool AudioTrack::fillStateFromFrame() {
_startedPosition = FramePosition(_stream); const auto position = FramePosition(_stream);
return (_startedPosition != kTimeUnknown); if (position == kTimeUnknown) {
return false;
}
_startedPosition = position;
return true;
} }
void AudioTrack::mixerInit() { void AudioTrack::mixerInit() {

View File

@ -52,6 +52,7 @@ private:
[[nodiscard]] bool initialized() const; [[nodiscard]] bool initialized() const;
[[nodiscard]] bool tryReadFirstFrame(Packet &&packet); [[nodiscard]] bool tryReadFirstFrame(Packet &&packet);
[[nodiscard]] bool fillStateFromFrame(); [[nodiscard]] bool fillStateFromFrame();
[[nodiscard]] bool processFirstFrame();
void mixerInit(); void mixerInit();
void mixerEnqueue(Packet &&packet); void mixerEnqueue(Packet &&packet);
void mixerForceToBuffer(); void mixerForceToBuffer();
@ -78,6 +79,9 @@ private:
// After that accessed from the main thread. // After that accessed from the main thread.
rpl::variable<crl::time> _playPosition; rpl::variable<crl::time> _playPosition;
// For initial frame skipping for an exact seek.
FramePointer _initialSkippingFrame;
}; };
} // namespace Streaming } // namespace Streaming

View File

@ -110,15 +110,8 @@ Stream File::Context::initStream(AVMediaType type) {
} }
const auto info = _formatContext->streams[index]; const auto info = _formatContext->streams[index];
result.codec = MakeCodecPointer(info);
if (!result.codec) {
return {};
}
if (type == AVMEDIA_TYPE_VIDEO) { if (type == AVMEDIA_TYPE_VIDEO) {
const auto codec = result.codec.get();
result.rotation = ReadRotationFromMetadata(info); result.rotation = ReadRotationFromMetadata(info);
result.dimensions = QSize(codec->width, codec->height);
} else if (type == AVMEDIA_TYPE_AUDIO) { } else if (type == AVMEDIA_TYPE_AUDIO) {
result.frequency = info->codecpar->sample_rate; result.frequency = info->codecpar->sample_rate;
if (!result.frequency) { if (!result.frequency) {
@ -126,6 +119,11 @@ Stream File::Context::initStream(AVMediaType type) {
} }
} }
result.codec = MakeCodecPointer(info);
if (!result.codec) {
return {};
}
result.frame = MakeFramePointer(); result.frame = MakeFramePointer();
if (!result.frame) { if (!result.frame) {
return {}; return {};

View File

@ -154,7 +154,6 @@ struct Stream {
// Video only. // Video only.
int rotation = 0; int rotation = 0;
QSize dimensions;
SwsContextPointer swsContext; SwsContextPointer swsContext;
}; };

View File

@ -47,10 +47,7 @@ private:
[[nodiscard]] bool interrupted() const; [[nodiscard]] bool interrupted() const;
[[nodiscard]] bool tryReadFirstFrame(Packet &&packet); [[nodiscard]] bool tryReadFirstFrame(Packet &&packet);
[[nodiscard]] bool fillStateFromFrame(); [[nodiscard]] bool fillStateFromFrame();
[[nodiscard]] bool fillStateFromFakeLastFrame(); [[nodiscard]] bool processFirstFrame();
[[nodiscard]] bool fillStateFromFrameTime(crl::time frameTime);
[[nodiscard]] QImage createFakeLastFrame() const;
[[nodiscard]] bool processFirstFrame(QImage frame);
void queueReadFrames(crl::time delay = 0); void queueReadFrames(crl::time delay = 0);
void readFrames(); void readFrames();
[[nodiscard]] bool readFrame(not_null<Frame*> frame); [[nodiscard]] bool readFrame(not_null<Frame*> frame);
@ -83,6 +80,9 @@ private:
bool _queued = false; bool _queued = false;
base::ConcurrentTimer _readFramesTimer; base::ConcurrentTimer _readFramesTimer;
// For initial frame skipping for an exact seek.
FramePointer _initialSkippingFrame;
}; };
VideoTrackObject::VideoTrackObject( VideoTrackObject::VideoTrackObject(
@ -259,10 +259,12 @@ bool VideoTrackObject::tryReadFirstFrame(Packet &&packet) {
auto frame = QImage(); auto frame = QImage();
if (const auto error = ReadNextFrame(_stream)) { if (const auto error = ReadNextFrame(_stream)) {
if (error.code() == AVERROR_EOF) { if (error.code() == AVERROR_EOF) {
if (!fillStateFromFakeLastFrame()) { if (!_initialSkippingFrame) {
return false; return false;
} }
return processFirstFrame(createFakeLastFrame()); // Return the last valid frame if we seek too far.
_stream.frame = std::move(_initialSkippingFrame);
return processFirstFrame();
} else if (error.code() != AVERROR(EAGAIN) || _noMoreData) { } else if (error.code() != AVERROR(EAGAIN) || _noMoreData) {
return false; return false;
} else { } else {
@ -271,22 +273,21 @@ bool VideoTrackObject::tryReadFirstFrame(Packet &&packet) {
} }
} else if (!fillStateFromFrame()) { } else if (!fillStateFromFrame()) {
return false; return false;
} else if (_syncTimePoint.trackTime < _options.position) {
// Seek was with AVSEEK_FLAG_BACKWARD so first we get old frames.
// Try skipping frames until one is after the requested position.
std::swap(_initialSkippingFrame, _stream.frame);
if (!_stream.frame) {
_stream.frame = MakeFramePointer();
}
return true;
} else {
return processFirstFrame();
} }
return processFirstFrame(ConvertFrame(_stream, QSize(), QImage()));
} }
QImage VideoTrackObject::createFakeLastFrame() const { bool VideoTrackObject::processFirstFrame() {
if (_stream.dimensions.isEmpty()) { auto frame = ConvertFrame(_stream, QSize(), QImage());
LOG(("Streaming Error: Can't seek to the end of the video "
"in case the codec doesn't provide valid dimensions."));
return QImage();
}
auto result = CreateImageForOriginalFrame(_stream.dimensions);
result.fill(Qt::black);
return result;
}
bool VideoTrackObject::processFirstFrame(QImage frame) {
if (frame.isNull()) { if (frame.isNull()) {
return false; return false;
} }
@ -312,20 +313,11 @@ crl::time VideoTrackObject::currentFramePosition() const {
} }
bool VideoTrackObject::fillStateFromFrame() { bool VideoTrackObject::fillStateFromFrame() {
return fillStateFromFrameTime(currentFramePosition()); const auto position = currentFramePosition();
} if (position == kTimeUnknown) {
bool VideoTrackObject::fillStateFromFakeLastFrame() {
return fillStateFromFrameTime(_stream.duration);
}
bool VideoTrackObject::fillStateFromFrameTime(crl::time frameTime) {
Expects(_syncTimePoint.trackTime == kTimeUnknown);
if (frameTime == kTimeUnknown) {
return false; return false;
} }
_syncTimePoint.trackTime = frameTime; _syncTimePoint.trackTime = position;
return true; return true;
} }