/* * Copyright 2011-2026 Branimir Karadzic. All rights reserved. * License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE */ #include "video_player.h" #include #include #include #include #include #include #include #include namespace stl = tinystl; BX_PRAGMA_DIAGNOSTIC_PUSH(); BX_PRAGMA_DIAGNOSTIC_IGNORED_MSVC(4127) // warning C4127: conditional expression is constant BX_PRAGMA_DIAGNOSTIC_IGNORED_MSVC(4244) // warning C4244: '=': conversion, possible loss of data BX_PRAGMA_DIAGNOSTIC_IGNORED_CLANG_GCC("-Wshadow") #include "l-smash/lsmash.h" BX_PRAGMA_DIAGNOSTIC_POP(); #include struct VideoPlayer::Impl { bgfx::VideoCodec::Enum codec = bgfx::VideoCodec::Count; uint8_t chroma = 1; uint8_t bitDepth = 8; uint8_t profile = 0; uint8_t level = 0; uint16_t width = 0; uint16_t height = 0; uint8_t maxDpbSlots = 0; uint8_t maxActiveReferences = 0; stl::vector parameterSets; lsmash_root_t* root = NULL; lsmash_file_parameters_t fp = {}; bool fpOpened = false; uint32_t trackId = 0; uint32_t lengthSize = 4; stl::vector annexBScratch; struct Frame { uint64_t ptsUs; bool isKeyframe; }; stl::vector frames; ~Impl() { if (NULL != root) { lsmash_destroy_root(root); root = NULL; } if (fpOpened) { lsmash_close_file(&fp); fpOpened = false; } } }; namespace { const uint32_t kMaxSubmitPerCall = 1; const int64_t kLookaheadUs = 2 * 1000000; const int64_t kSkipStepUs = 8 * 1000000; const float kRateStepFactor = 1.25892541f; const float kRateMin = 0.1f; const float kRateMax = 100.0f; void avccToAnnexB(stl::vector& _out, const uint8_t* _in, uint32_t _size, uint32_t _lengthSize) { uint32_t pos = 0; while (pos + _lengthSize <= _size) { uint32_t naluLen = 0; for (uint32_t ii = 0; ii < _lengthSize; ++ii) { naluLen = (naluLen << 8) | _in[pos + ii]; } pos += _lengthSize; if (pos + naluLen > _size) { break; } _out.push_back(0); _out.push_back(0); _out.push_back(1); _out.insert(_out.end(), _in + pos, _in + pos + naluLen); pos += naluLen; } } uint32_t appendNalAsAnnexB(stl::vector& _out, const uint8_t* _nal, uint32_t _nalLen) { if (NULL == _nal || 0 == _nalLen) { return 0; } _out.push_back(0); _out.push_back(0); _out.push_back(1); _out.insert(_out.end(), _nal, _nal + _nalLen); return 1; } void stripEmulationPrevention(stl::vector& _dst, const uint8_t* _src, uint32_t _size) { _dst.clear(); _dst.reserve(_size); uint32_t zeros = 0; for (uint32_t ii = 0; ii < _size; ++ii) { if (zeros >= 2 && 0x03 == _src[ii]) { zeros = 0; continue; } zeros = (0 == _src[ii]) ? zeros + 1 : 0; _dst.push_back(_src[ii]); } } bool parseAvcC(VideoPlayer::Impl& _video, const uint8_t* _data, uint32_t _size, uint32_t& _lengthSize) { const uint32_t kBoxHeaderSize = 8; if (NULL == _data || _size < kBoxHeaderSize + 7) { return false; } uint32_t pos = kBoxHeaderSize; ++pos; const uint8_t profileIdc = _data[pos++]; ++pos; const uint8_t levelIdc = _data[pos++]; _lengthSize = uint32_t(_data[pos++] & 0x03) + 1; const uint32_t numSps = uint32_t(_data[pos++] & 0x1f); _video.profile = profileIdc; _video.level = levelIdc; _video.chroma = 0; _video.bitDepth = 8; bool spsParsed = false; stl::vector rbsp; for (uint32_t ii = 0; ii < numSps; ++ii) { if (pos + 2 > _size) { return false; } const uint32_t nalLen = (uint32_t(_data[pos]) << 8) | uint32_t(_data[pos + 1]); pos += 2; if (pos + nalLen > _size) { return false; } appendNalAsAnnexB(_video.parameterSets, _data + pos, nalLen); if (!spsParsed && nalLen >= 1) { stripEmulationPrevention(rbsp, _data + pos, nalLen); h264::Bitstream bs; bs.init(rbsp.data(), rbsp.size() ); h264::NALHeader nal; if (h264::read_nal_header(&nal, &bs) && h264::NAL_UNIT_TYPE_SPS == nal.type) { h264::SPS sps = {}; h264::read_sps(&sps, &bs); _video.chroma = 3 == sps.chroma_format_idc ? 4 : 2 == sps.chroma_format_idc ? 2 : 0 ; _video.bitDepth = uint8_t(sps.bit_depth_luma_minus8 + 8); spsParsed = true; } } pos += nalLen; } if (pos + 1 > _size) { return false; } const uint32_t numPps = _data[pos++]; for (uint32_t ii = 0; ii < numPps; ++ii) { if (pos + 2 > _size) { return false; } const uint32_t nalLen = (uint32_t(_data[pos]) << 8) | uint32_t(_data[pos + 1]); pos += 2; if (pos + nalLen > _size) { return false; } appendNalAsAnnexB(_video.parameterSets, _data + pos, nalLen); pos += nalLen; } return true; } bool parseHvcC(VideoPlayer::Impl& _video, const uint8_t* _data, uint32_t _size, uint32_t& _lengthSize) { const uint32_t kBoxHeaderSize = 8; if (NULL == _data || _size < kBoxHeaderSize + 23) { return false; } uint32_t pos = kBoxHeaderSize; ++pos; const uint8_t profileByte = _data[pos++]; const uint8_t profileIdc = profileByte & 0x1f; pos += 10; const uint8_t levelIdc = _data[pos++]; pos += 3; const uint8_t chromaFormat = _data[pos++] & 0x03; const uint8_t bitDepthLuma = uint8_t( (_data[pos++] & 0x07) + 8); pos += 3; const uint8_t lengthSizeByte = _data[pos++]; _lengthSize = uint32_t(lengthSizeByte & 0x03) + 1; if (pos + 1 > _size) { return false; } const uint32_t numArrays = _data[pos++]; _video.profile = profileIdc; _video.level = levelIdc; _video.chroma = 3 == chromaFormat ? 4 : 2 == chromaFormat ? 2 : 0 ; _video.bitDepth = bitDepthLuma; stl::vector vps; stl::vector sps; stl::vector pps; for (uint32_t aa = 0; aa < numArrays; ++aa) { if (pos + 3 > _size) { return false; } const uint8_t arrayHdr = _data[pos++]; const uint8_t nalUnitType = arrayHdr & 0x3f; const uint32_t numNalus = (uint32_t(_data[pos]) << 8) | uint32_t(_data[pos + 1]); pos += 2; stl::vector* dst = NULL; switch (nalUnitType) { case 32: dst = &vps; break; case 33: dst = &sps; break; case 34: dst = &pps; break; default: break; } for (uint32_t nn = 0; nn < numNalus; ++nn) { if (pos + 2 > _size) { return false; } const uint32_t nalLen = (uint32_t(_data[pos]) << 8) | uint32_t(_data[pos + 1]); pos += 2; if (pos + nalLen > _size) { return false; } if (NULL != dst) { appendNalAsAnnexB(*dst, _data + pos, nalLen); } pos += nalLen; } } _video.parameterSets.insert(_video.parameterSets.end(), vps.begin(), vps.end() ); _video.parameterSets.insert(_video.parameterSets.end(), sps.begin(), sps.end() ); _video.parameterSets.insert(_video.parameterSets.end(), pps.begin(), pps.end() ); return true; } bool loadVideoFile(VideoPlayer::Impl& _video, const char* _path) { lsmash_root_t* root = lsmash_create_root(); if (NULL == root) { return false; } lsmash_file_parameters_t fp = {}; if (lsmash_open_file(_path, 1, &fp) < 0) { lsmash_destroy_root(root); return false; } lsmash_file_t* fh = lsmash_set_file(root, &fp); if (NULL == fh) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } if (lsmash_read_file(fh, &fp) < 0) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } lsmash_movie_parameters_t mp; lsmash_initialize_movie_parameters(&mp); if (lsmash_get_movie_parameters(root, &mp) < 0) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } uint32_t trackId = 0; lsmash_media_parameters_t md; for (uint32_t ii = 0; ii < mp.number_of_tracks; ++ii) { const uint32_t tid = lsmash_get_track_ID(root, ii + 1); if (0 == tid) { continue; } lsmash_initialize_media_parameters(&md); if (lsmash_get_media_parameters(root, tid, &md) < 0) { continue; } if (md.handler_type == ISOM_MEDIA_HANDLER_TYPE_VIDEO_TRACK) { trackId = tid; break; } } if (0 == trackId) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } lsmash_summary_t* sum = lsmash_get_summary(root, trackId, 1); if (NULL == sum) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } if (lsmash_check_codec_type_identical(sum->sample_type, ISOM_CODEC_TYPE_AVC1_VIDEO) || lsmash_check_codec_type_identical(sum->sample_type, ISOM_CODEC_TYPE_AVC3_VIDEO) ) { _video.codec = bgfx::VideoCodec::H264; } else if (lsmash_check_codec_type_identical(sum->sample_type, ISOM_CODEC_TYPE_HVC1_VIDEO) ) { _video.codec = bgfx::VideoCodec::H265; } else { lsmash_cleanup_summary(sum); lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } const lsmash_video_summary_t* vsum = (const lsmash_video_summary_t*)sum; _video.width = uint16_t(vsum->width); _video.height = uint16_t(vsum->height); _video.parameterSets.clear(); uint32_t lengthSize = 4; if (_video.codec == bgfx::VideoCodec::H264) { for (uint32_t ii = 0; ; ++ii) { lsmash_codec_specific_t* cs = lsmash_get_codec_specific_data(sum, ii + 1); if (NULL == cs) { break; } if (cs->type != LSMASH_CODEC_SPECIFIC_DATA_TYPE_ISOM_VIDEO_H264) { continue; } lsmash_codec_specific_t* csRaw = (cs->format == LSMASH_CODEC_SPECIFIC_FORMAT_UNSTRUCTURED) ? cs : lsmash_convert_codec_specific_format(cs, LSMASH_CODEC_SPECIFIC_FORMAT_UNSTRUCTURED) ; if (NULL == csRaw) { continue; } parseAvcC(_video, csRaw->data.unstructured, csRaw->size, lengthSize); if (csRaw != cs) { lsmash_destroy_codec_specific_data(csRaw); } break; } } if (_video.codec == bgfx::VideoCodec::H265) { for (uint32_t ii = 0; ; ++ii) { lsmash_codec_specific_t* cs = lsmash_get_codec_specific_data(sum, ii + 1); if (NULL == cs) { break; } if (cs->type != LSMASH_CODEC_SPECIFIC_DATA_TYPE_ISOM_VIDEO_HEVC) { continue; } lsmash_codec_specific_t* csRaw = (cs->format == LSMASH_CODEC_SPECIFIC_FORMAT_UNSTRUCTURED) ? cs : lsmash_convert_codec_specific_format(cs, LSMASH_CODEC_SPECIFIC_FORMAT_UNSTRUCTURED) ; if (NULL == csRaw) { continue; } parseHvcC(_video, csRaw->data.unstructured, csRaw->size, lengthSize); if (csRaw != cs) { lsmash_destroy_codec_specific_data(csRaw); } break; } } _video.maxDpbSlots = 16; _video.maxActiveReferences = 4; if (lsmash_construct_timeline(root, trackId) < 0) { lsmash_cleanup_summary(sum); lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } const uint32_t timescale = bx::max(md.timescale, 1); const uint32_t numSamples = lsmash_get_sample_count_in_media_timeline(root, trackId); _video.frames.reserve(numSamples); for (uint32_t ii = 0; ii < numSamples; ++ii) { lsmash_sample_t s = {}; if (0 != lsmash_get_sample_info_from_media_timeline(root, trackId, ii + 1, &s) ) { break; } VideoPlayer::Impl::Frame frame = {}; frame.ptsUs = s.cts * 1000000ull / timescale; frame.isKeyframe = 0 != (s.prop.ra_flags & ISOM_SAMPLE_RANDOM_ACCESS_FLAG_SYNC); _video.frames.push_back(frame); } lsmash_cleanup_summary(sum); if (_video.frames.empty() ) { lsmash_close_file(&fp); lsmash_destroy_root(root); return false; } _video.root = root; _video.fp = fp; _video.fpOpened = true; _video.trackId = trackId; _video.lengthSize = lengthSize; return true; } } // namespace VideoPlayer::VideoPlayer() : m_impl(NULL) , m_isOpen(false) , m_hasError(false) , m_paused(false) , m_pendingSet(false) , m_width(0) , m_height(0) , m_durationUs(0) , m_playbackTimeUs(0) , m_lastTickHpc(0) , m_nextAuToSubmit(0) , m_lastSetIdr(UINT32_MAX) , m_playbackRate(1.0f) , m_aMarkerUs(-1) , m_bMarkerUs(-1) , m_videoTexture(BGFX_INVALID_HANDLE) { } VideoPlayer::~VideoPlayer() { close(); } bool VideoPlayer::isVideoExt(const bx::StringView& _ext) { return 0 == bx::strCmpI(_ext, "mp4") || 0 == bx::strCmpI(_ext, "m4v") ; } bool VideoPlayer::open(const char* _filePath) { close(); m_impl = BX_NEW(entry::getAllocator(), Impl); if (NULL == m_impl) { m_hasError = true; return false; } if (!loadVideoFile(*m_impl, _filePath) ) { bx::deleteObject(entry::getAllocator(), m_impl); m_impl = NULL; m_hasError = true; return false; } m_width = m_impl->width; m_height = m_impl->height; const bgfx::Caps* caps = bgfx::getCaps(); const uint32_t codecCaps = caps->codecs[m_impl->codec]; const bool codecAvailable = true && 0 != (caps->supported & BGFX_CAPS_VIDEO_DECODE) && 0 != (codecCaps & BGFX_CAPS_VIDEO_CODEC_BIT_8) && 0 != (codecCaps & BGFX_CAPS_VIDEO_CODEC_CHROMA_420) ; const bool targetSupported = 0 != (caps->formats[bgfx::TextureFormat::BGRA8] & BGFX_CAPS_FORMAT_TEXTURE_VIDEO_DECODE_DST); const bool codecValid = bgfx::isVideoCodecValid( m_impl->codec , m_impl->chroma , m_impl->bitDepth , m_impl->width , m_impl->height , m_impl->maxDpbSlots , m_impl->maxActiveReferences ); if (!codecAvailable || !targetSupported || !codecValid) { bx::deleteObject(entry::getAllocator(), m_impl); m_impl = NULL; m_hasError = true; return false; } const uint32_t psSize = uint32_t(m_impl->parameterSets.size() ); const uint32_t totalSize = uint32_t(sizeof(bgfx::VideoDecoderInit) ) + psSize; const bgfx::Memory* mem = bgfx::alloc(totalSize); bgfx::VideoDecoderInit* decInit = (bgfx::VideoDecoderInit*)mem->data; decInit->magic = BX_MAKEFOURCC('V', 'D', 'I', 0x0); decInit->codec = m_impl->codec; decInit->parameterSets = mem->data + sizeof(bgfx::VideoDecoderInit); decInit->parameterSetsSize = psSize; decInit->flags = 0; decInit->cachedAuBytes = 0; bx::memCopy( mem->data + sizeof(bgfx::VideoDecoderInit) , m_impl->parameterSets.data() , psSize ); m_videoTexture = bgfx::createTexture2D( m_width , m_height , false , 1 , bgfx::TextureFormat::BGRA8 , BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP , mem ); if (!bgfx::isValid(m_videoTexture) ) { bx::deleteObject(entry::getAllocator(), m_impl); m_impl = NULL; m_hasError = true; return false; } m_durationUs = m_impl->frames.back().ptsUs; m_playbackTimeUs = 0; m_lastTickHpc = bx::getHPCounter(); m_nextAuToSubmit = 0; m_lastSetIdr = UINT32_MAX; m_playbackRate = 1.0f; m_paused = false; m_pendingSet = false; m_aMarkerUs = -1; m_bMarkerUs = -1; m_isOpen = true; m_hasError = false; submitAccessUnitsUpTo(kLookaheadUs); return true; } void VideoPlayer::close() { if (bgfx::isValid(m_videoTexture) ) { bgfx::destroy(m_videoTexture); m_videoTexture = BGFX_INVALID_HANDLE; } if (NULL != m_impl) { bx::deleteObject(entry::getAllocator(), m_impl); m_impl = NULL; } m_isOpen = false; m_hasError = false; m_paused = false; m_pendingSet = false; m_width = 0; m_height = 0; m_durationUs = 0; m_playbackTimeUs = 0; m_nextAuToSubmit = 0; m_lastSetIdr = UINT32_MAX; m_playbackRate = 1.0f; m_aMarkerUs = -1; m_bMarkerUs = -1; } bgfx::VideoCodec::Enum VideoPlayer::codec() const { return NULL != m_impl ? m_impl->codec : bgfx::VideoCodec::Count; } uint32_t VideoPlayer::findIdrAtOrBefore(uint64_t _ptsUs) const { if (NULL == m_impl || m_impl->frames.empty() ) { return 0; } uint32_t lastIdr = 0; for (uint32_t ii = 0; ii < uint32_t(m_impl->frames.size() ); ++ii) { if (m_impl->frames[ii].ptsUs > _ptsUs) { break; } if (m_impl->frames[ii].isKeyframe) { lastIdr = ii; } } return lastIdr; } uint32_t VideoPlayer::findIdrAtOrAfter(uint64_t _ptsUs) const { if (NULL == m_impl || m_impl->frames.empty() ) { return 0; } const uint32_t numFrames = uint32_t(m_impl->frames.size() ); for (uint32_t ii = 0; ii < numFrames; ++ii) { if (m_impl->frames[ii].ptsUs >= _ptsUs && m_impl->frames[ii].isKeyframe) { return ii; } } return numFrames; } uint32_t VideoPlayer::findFrameAtOrBefore(uint64_t _ptsUs) const { if (NULL == m_impl || m_impl->frames.empty() ) { return 0; } const uint32_t numFrames = uint32_t(m_impl->frames.size() ); uint32_t result = 0; for (uint32_t ii = 0; ii < numFrames; ++ii) { if (m_impl->frames[ii].ptsUs > _ptsUs) { break; } result = ii; } return result; } void VideoPlayer::submitDecodeFrame(uint32_t _index, bool _setPosition) { const VideoPlayer::Impl::Frame& f = m_impl->frames[_index]; lsmash_sample_t* s = lsmash_get_sample_from_media_timeline( m_impl->root , m_impl->trackId , _index + 1 ); if (NULL == s) { return; } struct Payload { bgfx::VideoDecoderFrame frame; bgfx::VideoDecoderAu au; }; stl::vector& scratch = m_impl->annexBScratch; scratch.clear(); avccToAnnexB(scratch, s->data, s->length, m_impl->lengthSize); const uint32_t bitstreamSize = uint32_t(scratch.size() ); const uint32_t totalSize = uint32_t(sizeof(Payload) ) + bitstreamSize; const bgfx::Memory* mem = bgfx::alloc(totalSize); Payload* dstPayload = (Payload*)mem->data; uint8_t* dstBitstream = (uint8_t*)(dstPayload + 1); bx::memCopy(dstBitstream, scratch.data(), bitstreamSize); dstPayload->au.size = bitstreamSize; dstPayload->au.ptsUs = int64_t(f.ptsUs); dstPayload->frame.magic = BX_MAKEFOURCC('V', 'D', 'F', 0x0); dstPayload->frame.bitstream = dstBitstream; dstPayload->frame.aus = &dstPayload->au; dstPayload->frame.numAus = 1; dstPayload->frame.presentationTimeUs = 0; dstPayload->frame.flags = _setPosition ? BGFX_VIDEO_DECODE_FRAME_SET : BGFX_VIDEO_DECODE_FRAME_NONE ; bgfx::updateTexture2D( m_videoTexture , 0 , 0 , 0, 0 , m_width , m_height , mem ); lsmash_delete_sample(s); } void VideoPlayer::submitAccessUnitsUpTo(int64_t _untilPtsUs) { if (NULL == m_impl) { return; } uint32_t submitted = 0; while (m_nextAuToSubmit < uint32_t(m_impl->frames.size() ) && int64_t(m_impl->frames[m_nextAuToSubmit].ptsUs) <= _untilPtsUs && submitted < kMaxSubmitPerCall) { submitDecodeFrame(m_nextAuToSubmit, m_pendingSet); m_pendingSet = false; ++m_nextAuToSubmit; ++submitted; } } void VideoPlayer::submitPresentationTick(int64_t _presentationTimeUs) { bgfx::VideoDecoderFrame desc = {}; desc.magic = BX_MAKEFOURCC('V', 'D', 'F', 0x0); desc.bitstream = NULL; desc.aus = NULL; desc.numAus = 0; desc.presentationTimeUs = _presentationTimeUs; desc.flags = BGFX_VIDEO_DECODE_FRAME_NONE; bgfx::updateTexture2D( m_videoTexture , 0 , 0 , 0, 0 , m_width , m_height , bgfx::copy(&desc, sizeof(desc) ) ); } void VideoPlayer::tick() { if (!m_isOpen) { return; } const int64_t nowHpc = bx::getHPCounter(); const int64_t deltaHpc = nowHpc - m_lastTickHpc; m_lastTickHpc = nowHpc; if (!m_paused) { m_lastSetIdr = UINT32_MAX; const uint64_t deltaUs = uint64_t(double(deltaHpc) * 1000000.0 * double(m_playbackRate) / double(bx::getHPFrequency() ) ); m_playbackTimeUs += deltaUs; if (m_aMarkerUs >= 0 && m_bMarkerUs > m_aMarkerUs && int64_t(m_playbackTimeUs) >= m_bMarkerUs) { seekTo(uint64_t(m_aMarkerUs) ); } else if (m_playbackTimeUs > m_durationUs) { seekTo(m_aMarkerUs >= 0 ? uint64_t(m_aMarkerUs) : 0); } } submitPresentationTick(int64_t(m_playbackTimeUs) ); submitAccessUnitsUpTo(int64_t(m_playbackTimeUs) + kLookaheadUs); } void VideoPlayer::seekTo(uint64_t _targetUs) { if (!m_isOpen || NULL == m_impl) { return; } const uint64_t target = bx::min(_targetUs, m_durationUs); const uint32_t idr = findIdrAtOrBefore(target); const uint32_t targetFrame = findFrameAtOrBefore(target); const bool needReprime = targetFrame < m_nextAuToSubmit || idr > m_nextAuToSubmit ; if (needReprime && idr != m_lastSetIdr) { m_nextAuToSubmit = idr; m_pendingSet = true; m_lastSetIdr = idr; } m_playbackTimeUs = m_paused ? target : m_impl->frames[idr].ptsUs ; m_lastTickHpc = bx::getHPCounter(); } void VideoPlayer::seekRelative(int64_t _deltaUs) { if (!m_isOpen || NULL == m_impl) { return; } m_lastSetIdr = UINT32_MAX; int64_t target = int64_t(m_playbackTimeUs) + _deltaUs; int64_t lo = 0; int64_t hi = int64_t(m_durationUs); if (m_aMarkerUs >= 0 && m_bMarkerUs > m_aMarkerUs) { lo = m_aMarkerUs; hi = m_bMarkerUs; } if (_deltaUs > 0 && target > hi) { seekTo(uint64_t(lo) ); return; } target = bx::clamp(target, lo, hi); if (_deltaUs > 0) { const uint32_t idrBefore = findIdrAtOrBefore(uint64_t(target) ); const uint64_t idrBeforePts = m_impl->frames[idrBefore].ptsUs; if (idrBeforePts <= m_playbackTimeUs) { const uint32_t numFrames = uint32_t(m_impl->frames.size() ); const uint32_t idrAfter = findIdrAtOrAfter(m_playbackTimeUs + 1); if (idrAfter < numFrames) { m_nextAuToSubmit = idrAfter; m_pendingSet = true; m_lastSetIdr = idrAfter; m_playbackTimeUs = m_impl->frames[idrAfter].ptsUs; m_lastTickHpc = bx::getHPCounter(); return; } return; } } seekTo(uint64_t(target) ); } void VideoPlayer::restart() { if (!m_isOpen) { return; } m_lastSetIdr = UINT32_MAX; seekTo(m_aMarkerUs >= 0 ? uint64_t(m_aMarkerUs) : 0); } void VideoPlayer::togglePause() { if (!m_isOpen) { return; } m_paused = !m_paused; m_lastTickHpc = bx::getHPCounter(); } void VideoPlayer::rateUp() { if (!m_isOpen) { return; } m_playbackRate = bx::min(m_playbackRate * kRateStepFactor, kRateMax); } void VideoPlayer::rateDown() { if (!m_isOpen) { return; } m_playbackRate = bx::max(m_playbackRate / kRateStepFactor, kRateMin); } void VideoPlayer::cycleAbMarker() { if (!m_isOpen) { return; } if (m_aMarkerUs < 0) { m_aMarkerUs = int64_t(m_playbackTimeUs); m_bMarkerUs = -1; } else if (m_bMarkerUs < 0) { int64_t b = int64_t(m_playbackTimeUs); if (b < m_aMarkerUs) { m_bMarkerUs = m_aMarkerUs; m_aMarkerUs = b; } else { m_bMarkerUs = b; } } else { m_aMarkerUs = -1; m_bMarkerUs = -1; } }