Path: blob/master/modules/theora/video_stream_theora.cpp
10277 views
/**************************************************************************/1/* video_stream_theora.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "video_stream_theora.h"3132#include "core/config/project_settings.h"33#include "core/io/image.h"34#include "scene/resources/image_texture.h"3536#include "thirdparty/misc/yuv2rgb.h"3738int VideoStreamPlaybackTheora::buffer_data() {39char *buffer = ogg_sync_buffer(&oy, 4096);4041uint64_t bytes = file->get_buffer((uint8_t *)buffer, 4096);42ogg_sync_wrote(&oy, bytes);43return bytes;44}4546int VideoStreamPlaybackTheora::queue_page(ogg_page *page) {47ogg_stream_pagein(&to, page);48if (to.e_o_s) {49theora_eos = true;50}51if (has_audio) {52ogg_stream_pagein(&vo, page);53if (vo.e_o_s) {54vorbis_eos = true;55}56}57return 0;58}5960int VideoStreamPlaybackTheora::read_page(ogg_page *page) {61int ret = 0;6263while (ret <= 0) {64ret = ogg_sync_pageout(&oy, page);65if (ret <= 0) {66int bytes = buffer_data();67if (bytes == 0) {68return 0;69}70}71}7273return ret;74}7576double VideoStreamPlaybackTheora::get_page_time(ogg_page *page) {77uint64_t granulepos = ogg_page_granulepos(page);78int page_serialno = ogg_page_serialno(page);79double page_time = -1;8081if (page_serialno == to.serialno) {82page_time = th_granule_time(td, granulepos);83}84if (has_audio && page_serialno == vo.serialno) {85page_time = vorbis_granule_time(&vd, granulepos);86}8788return page_time;89}9091// Read one buffer worth of pages and feed them to the streams.92int VideoStreamPlaybackTheora::feed_pages() {93int pages = 0;94ogg_page og;9596while (pages == 0) {97while (ogg_sync_pageout(&oy, &og) > 0) {98queue_page(&og);99pages++;100}101if (pages == 0) {102int bytes = buffer_data();103if (bytes == 0) {104break;105}106}107}108109return pages;110}111112// Seek the video and audio streams simultaneously to find the granulepos where we should start decoding.113// It will return the position where we should start reading pages, and the video and audio granulepos.114int64_t VideoStreamPlaybackTheora::seek_streams(double p_time, int64_t &cur_video_granulepos, int64_t &cur_audio_granulepos) {115// Backtracking less than this is probably a waste of time.116const int64_t min_seek = 512 * 1024;117int64_t target_video_granulepos;118int64_t target_audio_granulepos;119double target_time = 0;120int64_t seek_pos;121122// Make a guess where we should start reading in the file, and scan from there.123// We base the guess on the mean bitrate of the streams. It would be theoretically faster to use the bisect method but124// in practice there's a lot of linear scanning to do to find the right pages.125// We want to catch the previous keyframe to the seek time. Since we only know the max GOP, we use that.126if (p_time == -1) { // This is a special case to find the last packets and calculate the video length.127seek_pos = MAX(stream_data_size - min_seek, stream_data_offset);128target_video_granulepos = INT64_MAX;129target_audio_granulepos = INT64_MAX;130} else {131int64_t video_frame = (int64_t)(p_time / frame_duration);132target_video_granulepos = MAX(1LL, video_frame - (1LL << ti.keyframe_granule_shift)) << ti.keyframe_granule_shift;133target_audio_granulepos = 0;134seek_pos = MAX(((target_video_granulepos >> ti.keyframe_granule_shift) - 1) * frame_duration * stream_data_size / stream_length, stream_data_offset);135target_time = th_granule_time(td, target_video_granulepos);136if (has_audio) {137target_audio_granulepos = video_frame * frame_duration * vi.rate;138target_time = MIN(target_time, vorbis_granule_time(&vd, target_audio_granulepos));139}140}141142int64_t video_seek_pos = seek_pos;143int64_t audio_seek_pos = seek_pos;144double backtrack_time = 0;145bool video_catch = false;146bool audio_catch = false;147int64_t last_video_granule_seek_pos = seek_pos;148int64_t last_audio_granule_seek_pos = seek_pos;149150cur_video_granulepos = -1;151cur_audio_granulepos = -1;152153while (!video_catch || (has_audio && !audio_catch)) { // Backtracking loop154if (seek_pos < stream_data_offset) {155seek_pos = stream_data_offset;156}157file->seek(seek_pos);158ogg_sync_reset(&oy);159160backtrack_time = 0;161last_video_granule_seek_pos = seek_pos;162last_audio_granule_seek_pos = seek_pos;163while (!video_catch || (has_audio && !audio_catch)) { // Page scanning loop164ogg_page page;165uint64_t last_seek_pos = file->get_position() - oy.fill + oy.returned;166int ret = read_page(&page);167if (ret <= 0) { // End of file.168if (seek_pos < stream_data_offset) { // We've already searched the whole file169return -1;170}171seek_pos -= min_seek;172break;173}174int64_t cur_granulepos = ogg_page_granulepos(&page);175if (cur_granulepos >= 0) {176int page_serialno = ogg_page_serialno(&page);177if (!video_catch && page_serialno == to.serialno) {178if (cur_granulepos >= target_video_granulepos) {179video_catch = true;180if (cur_video_granulepos < 0) {181// Adding 1s helps catching the start of the page and avoids backtrack_time = 0.182backtrack_time = MAX(backtrack_time, 1 + th_granule_time(td, cur_granulepos) - target_time);183}184} else {185video_seek_pos = last_video_granule_seek_pos;186cur_video_granulepos = cur_granulepos;187}188last_video_granule_seek_pos = last_seek_pos;189}190if ((has_audio && !audio_catch) && page_serialno == vo.serialno) {191if (cur_granulepos >= target_audio_granulepos) {192audio_catch = true;193if (cur_audio_granulepos < 0) {194// Adding 1s helps catching the start of the page and avoids backtrack_time = 0.195backtrack_time = MAX(backtrack_time, 1 + vorbis_granule_time(&vd, cur_granulepos) - target_time);196}197} else {198audio_seek_pos = last_audio_granule_seek_pos;199cur_audio_granulepos = cur_granulepos;200}201last_audio_granule_seek_pos = last_seek_pos;202}203}204}205if (backtrack_time > 0) {206if (seek_pos <= stream_data_offset) {207break;208}209int64_t delta_seek = MAX(backtrack_time * stream_data_size / stream_length, min_seek);210seek_pos -= delta_seek;211}212video_catch = cur_video_granulepos != -1;213audio_catch = cur_audio_granulepos != -1;214}215216if (cur_video_granulepos < (1LL << ti.keyframe_granule_shift)) {217video_seek_pos = stream_data_offset;218cur_video_granulepos = 1LL << ti.keyframe_granule_shift;219}220if (has_audio) {221if (cur_audio_granulepos == -1) {222audio_seek_pos = stream_data_offset;223cur_audio_granulepos = 0;224}225seek_pos = MIN(video_seek_pos, audio_seek_pos);226} else {227seek_pos = video_seek_pos;228}229230return seek_pos;231}232233void VideoStreamPlaybackTheora::video_write(th_ycbcr_buffer yuv) {234uint8_t *w = frame_data.ptrw();235char *dst = (char *)w;236uint32_t y_offset = region.position.y * yuv[0].stride + region.position.x;237uint32_t uv_offset = 0;238239if (px_fmt == TH_PF_444) {240uv_offset += region.position.y * yuv[1].stride + region.position.x;241yuv444_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);242} else if (px_fmt == TH_PF_422) {243uv_offset += region.position.y * yuv[1].stride + region.position.x / 2;244yuv422_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);245} else if (px_fmt == TH_PF_420) {246uv_offset += region.position.y * yuv[1].stride / 2 + region.position.x / 2;247yuv420_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);248}249250Ref<Image> img;251img.instantiate(region.size.x, region.size.y, false, Image::FORMAT_RGBA8, frame_data); //zero copy image creation252253texture->update(img); // Zero-copy send to rendering server.254}255256void VideoStreamPlaybackTheora::clear() {257if (!file.is_null()) {258file.unref();259}260if (has_audio) {261vorbis_block_clear(&vb);262vorbis_dsp_clear(&vd);263vorbis_comment_clear(&vc);264vorbis_info_clear(&vi);265ogg_stream_clear(&vo);266if (audio_buffer_size) {267memdelete_arr(audio_buffer);268}269}270if (has_video) {271th_decode_free(td);272th_comment_clear(&tc);273th_info_clear(&ti);274ogg_stream_clear(&to);275ogg_sync_clear(&oy);276}277278audio_buffer = nullptr;279playing = false;280has_video = false;281has_audio = false;282theora_eos = false;283vorbis_eos = false;284}285286void VideoStreamPlaybackTheora::find_streams(th_setup_info *&ts) {287ogg_stream_state test;288ogg_packet op;289ogg_page og;290int stateflag = 0;291int audio_track_skip = audio_track;292293/* Only interested in Vorbis/Theora streams */294while (!stateflag) {295int ret = buffer_data();296if (!ret) {297break;298}299while (ogg_sync_pageout(&oy, &og) > 0) {300/* is this a mandated initial header? If not, stop parsing */301if (!ogg_page_bos(&og)) {302/* don't leak the page; get it into the appropriate stream */303queue_page(&og);304stateflag = 1;305break;306}307308ogg_stream_init(&test, ogg_page_serialno(&og));309ogg_stream_pagein(&test, &og);310ogg_stream_packetout(&test, &op);311312/* identify the codec: try theora */313if (!has_video && th_decode_headerin(&ti, &tc, &ts, &op) >= 0) {314/* it is theora */315memcpy(&to, &test, sizeof(test));316has_video = true;317} else if (!has_audio && vorbis_synthesis_headerin(&vi, &vc, &op) >= 0) {318/* it is vorbis */319if (audio_track_skip) {320vorbis_info_clear(&vi);321vorbis_comment_clear(&vc);322ogg_stream_clear(&test);323vorbis_info_init(&vi);324vorbis_comment_init(&vc);325audio_track_skip--;326} else {327memcpy(&vo, &test, sizeof(test));328has_audio = true;329}330} else {331/* whatever it is, we don't care about it */332ogg_stream_clear(&test);333}334}335}336}337338void VideoStreamPlaybackTheora::read_headers(th_setup_info *&ts) {339ogg_packet op;340int theora_header_packets = 1;341int vorbis_header_packets = 1;342343/* we're expecting more header packets. */344while (theora_header_packets < 3 || (has_audio && vorbis_header_packets < 3)) {345/* look for further theora headers */346// The API says there can be more than three but only three are mandatory.347while (theora_header_packets < 3 && ogg_stream_packetout(&to, &op) > 0) {348if (th_decode_headerin(&ti, &tc, &ts, &op) > 0) {349theora_header_packets++;350}351}352353/* look for more vorbis header packets */354while (has_audio && vorbis_header_packets < 3 && ogg_stream_packetout(&vo, &op) > 0) {355if (!vorbis_synthesis_headerin(&vi, &vc, &op)) {356vorbis_header_packets++;357}358}359360/* The header pages/packets will arrive before anything else we care about, or the stream is not obeying spec */361if (theora_header_packets < 3 || (has_audio && vorbis_header_packets < 3)) {362ogg_page page;363if (read_page(&page)) {364queue_page(&page);365} else {366fprintf(stderr, "End of file while searching for codec headers.\n");367break;368}369}370}371372has_video = theora_header_packets == 3;373has_audio = vorbis_header_packets == 3;374}375376void VideoStreamPlaybackTheora::set_file(const String &p_file) {377ERR_FAIL_COND(playing);378th_setup_info *ts = nullptr;379380clear();381382file = FileAccess::open(p_file, FileAccess::READ);383ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_file + "'.");384385file_name = p_file;386387ogg_sync_init(&oy);388389/* init supporting Vorbis structures needed in header parsing */390vorbis_info_init(&vi);391vorbis_comment_init(&vc);392393/* init supporting Theora structures needed in header parsing */394th_comment_init(&tc);395th_info_init(&ti);396397/* Zero stream state structs so they can be checked later. */398memset(&to, 0, sizeof(to));399memset(&vo, 0, sizeof(vo));400401/* Ogg file open; parse the headers */402find_streams(ts);403read_headers(ts);404405if (!has_audio) {406vorbis_comment_clear(&vc);407vorbis_info_clear(&vi);408if (!ogg_stream_check(&vo)) {409ogg_stream_clear(&vo);410}411}412413// One video stream is mandatory.414if (!has_video) {415th_setup_free(ts);416th_comment_clear(&tc);417th_info_clear(&ti);418if (!ogg_stream_check(&to)) {419ogg_stream_clear(&to);420}421file.unref();422return;423}424425/* And now we have it all. Initialize decoders. */426td = th_decode_alloc(&ti, ts);427th_setup_free(ts);428px_fmt = ti.pixel_fmt;429switch (ti.pixel_fmt) {430case TH_PF_420:431case TH_PF_422:432case TH_PF_444:433break;434default:435WARN_PRINT(" video\n (UNKNOWN Chroma sampling!)\n");436break;437}438th_decode_ctl(td, TH_DECCTL_GET_PPLEVEL_MAX, &pp_level_max, sizeof(pp_level_max));439pp_level = 0;440th_decode_ctl(td, TH_DECCTL_SET_PPLEVEL, &pp_level, sizeof(pp_level));441pp_inc = 0;442443size.x = ti.frame_width;444size.y = ti.frame_height;445region.position.x = ti.pic_x;446region.position.y = ti.pic_y;447region.size.x = ti.pic_width;448region.size.y = ti.pic_height;449450Ref<Image> img = Image::create_empty(region.size.x, region.size.y, false, Image::FORMAT_RGBA8);451texture->set_image(img);452frame_data.resize(region.size.x * region.size.y * 4);453454frame_duration = (double)ti.fps_denominator / ti.fps_numerator;455456if (has_audio) {457vorbis_synthesis_init(&vd, &vi);458vorbis_block_init(&vd, &vb);459audio_buffer_size = MIN(vi.channels, 8) * 1024;460audio_buffer = memnew_arr(float, audio_buffer_size);461}462463stream_data_offset = file->get_position() - oy.fill + oy.returned;464stream_data_size = file->get_length() - stream_data_offset;465466// Sync to last page to find video length.467int64_t seek_pos = MAX(stream_data_offset, (int64_t)file->get_length() - 64 * 1024);468int64_t video_granulepos = INT64_MAX;469int64_t audio_granulepos = INT64_MAX;470file->seek(seek_pos);471seek_pos = seek_streams(-1, video_granulepos, audio_granulepos);472file->seek(seek_pos);473ogg_sync_reset(&oy);474475stream_length = 0;476ogg_page page;477while (read_page(&page) > 0) {478// Use MAX because, even though pages are ordered, page time can be -1479// for pages without full frames. Streams could be truncated too.480stream_length = MAX(stream_length, get_page_time(&page));481}482483seek(0);484}485486double VideoStreamPlaybackTheora::get_time() const {487// FIXME: AudioServer output latency was fixed in af9bb0e, previously it used to488// systematically return 0. Now that it gives a proper latency, it broke this489// code where the delay compensation likely never really worked.490return time - /* AudioServer::get_singleton()->get_output_latency() - */ delay_compensation;491}492493Ref<Texture2D> VideoStreamPlaybackTheora::get_texture() const {494return texture;495}496497void VideoStreamPlaybackTheora::update(double p_delta) {498if (file.is_null()) {499return;500}501502if (!playing || paused) {503return;504}505506time += p_delta;507508double comp_time = get_time();509bool audio_ready = false;510511// Read data until we fill the audio buffer and get a new video frame.512while ((!audio_ready && !audio_done) || (!video_ready && !video_done)) {513ogg_packet op;514515while (!audio_ready && !audio_done) {516// Send remaining frames517if (!send_audio()) {518audio_ready = true;519break;520}521522float **pcm;523int ret = vorbis_synthesis_pcmout(&vd, &pcm);524if (ret > 0) {525int frames_read = 0;526while (frames_read < ret) {527int m = MIN(audio_buffer_size / vi.channels, ret - frames_read);528int count = 0;529for (int j = 0; j < m; j++) {530for (int i = 0; i < vi.channels; i++) {531audio_buffer[count++] = pcm[i][frames_read + j];532}533}534frames_read += m;535audio_ptr_end = m;536if (!send_audio()) {537audio_ready = true;538break;539}540}541vorbis_synthesis_read(&vd, frames_read);542} else {543/* no pending audio; is there a pending packet to decode? */544if (ogg_stream_packetout(&vo, &op) > 0) {545if (vorbis_synthesis(&vb, &op) == 0) { /* test for success! */546vorbis_synthesis_blockin(&vd, &vb);547}548} else { /* we need more data; break out to suck in another page */549audio_done = vorbis_eos;550break;551}552}553}554555while (!video_ready && !video_done) {556if (ogg_stream_packetout(&to, &op) > 0) {557if (op.granulepos >= 0) {558th_decode_ctl(td, TH_DECCTL_SET_GRANPOS, &op.granulepos, sizeof(op.granulepos));559}560int64_t videobuf_granulepos;561int ret = th_decode_packetin(td, &op, &videobuf_granulepos);562if (ret == 0 || ret == TH_DUPFRAME) {563next_frame_time = th_granule_time(td, videobuf_granulepos);564if (next_frame_time > comp_time) {565dup_frame = (ret == TH_DUPFRAME);566video_ready = true;567} else {568/*If we are too slow, reduce the pp level.*/569pp_inc = pp_level > 0 ? -1 : 0;570}571}572} else { /* we need more data; break out to suck in another page */573video_done = theora_eos;574break;575}576}577578if (!video_ready || !audio_ready) {579int ret = feed_pages();580if (ret == 0) {581vorbis_eos = true;582theora_eos = true;583break;584}585}586587double tdiff = next_frame_time - comp_time;588/*If we have lots of extra time, increase the post-processing level.*/589if (tdiff > ti.fps_denominator * 0.25 / ti.fps_numerator) {590pp_inc = pp_level < pp_level_max ? 1 : 0;591} else if (tdiff < ti.fps_denominator * 0.05 / ti.fps_numerator) {592pp_inc = pp_level > 0 ? -1 : 0;593}594}595596if (!video_ready && video_done && audio_done) {597stop();598return;599}600601// Wait for the last frame to end before rendering the next one.602if (video_ready && comp_time >= current_frame_time) {603if (!dup_frame) {604th_ycbcr_buffer yuv;605th_decode_ycbcr_out(td, yuv);606video_write(yuv);607}608dup_frame = false;609video_ready = false;610current_frame_time = next_frame_time;611}612}613614void VideoStreamPlaybackTheora::play() {615if (playing) {616return;617}618619playing = true;620delay_compensation = GLOBAL_GET("audio/video/video_delay_compensation_ms");621delay_compensation /= 1000.0;622}623624void VideoStreamPlaybackTheora::stop() {625playing = false;626seek(0);627}628629bool VideoStreamPlaybackTheora::is_playing() const {630return playing;631}632633void VideoStreamPlaybackTheora::set_paused(bool p_paused) {634paused = p_paused;635}636637bool VideoStreamPlaybackTheora::is_paused() const {638return paused;639}640641double VideoStreamPlaybackTheora::get_length() const {642return stream_length;643}644645double VideoStreamPlaybackTheora::get_playback_position() const {646return get_time();647}648649void VideoStreamPlaybackTheora::seek(double p_time) {650if (file.is_null()) {651return;652}653if (p_time >= stream_length) {654return;655}656657video_ready = false;658next_frame_time = 0;659current_frame_time = -1;660dup_frame = false;661video_done = false;662audio_done = !has_audio;663theora_eos = false;664vorbis_eos = false;665audio_ptr_start = 0;666audio_ptr_end = 0;667668ogg_stream_reset(&to);669if (has_audio) {670ogg_stream_reset(&vo);671vorbis_synthesis_restart(&vd);672}673674int64_t seek_pos;675int64_t video_granulepos;676int64_t audio_granulepos;677// Find the granules we need so we can start playing at the seek time.678seek_pos = seek_streams(p_time, video_granulepos, audio_granulepos);679if (seek_pos < 0) {680return;681}682file->seek(seek_pos);683ogg_sync_reset(&oy);684685time = p_time;686687double last_audio_time = 0;688double last_video_time = 0;689bool first_frame_decoded = false;690bool start_audio = (audio_granulepos == 0);691bool start_video = (video_granulepos == (1LL << ti.keyframe_granule_shift));692bool keyframe_found = false;693uint64_t current_frame = 0;694695// Read from the streams skipping pages until we reach the granules we want. We won't skip pages from both video and696// audio streams, only one of them, until decoding of both starts.697// video_granulepos and audio_granulepos are guaranteed to be found by checking the granulepos in the packets, no698// need to keep track of packets with granulepos == -1 until decoding starts.699while ((has_audio && last_audio_time < p_time) || (last_video_time <= p_time)) {700ogg_packet op;701if (feed_pages() == 0) {702break;703}704while (has_audio && last_audio_time < p_time && ogg_stream_packetout(&vo, &op) > 0) {705if (start_audio) {706if (vorbis_synthesis(&vb, &op) == 0) { /* test for success! */707vorbis_synthesis_blockin(&vd, &vb);708float **pcm;709int samples_left = ceil((p_time - last_audio_time) * vi.rate);710int samples_read = vorbis_synthesis_pcmout(&vd, &pcm);711int samples_consumed = MIN(samples_left, samples_read);712vorbis_synthesis_read(&vd, samples_consumed);713last_audio_time += (double)samples_consumed / vi.rate;714}715} else if (op.granulepos >= audio_granulepos) {716last_audio_time = vorbis_granule_time(&vd, op.granulepos);717// Start tracking audio now. This won't produce any samples but will update the decoder state.718if (vorbis_synthesis_trackonly(&vb, &op) == 0) {719vorbis_synthesis_blockin(&vd, &vb);720}721start_audio = true;722}723}724while (last_video_time <= p_time && ogg_stream_packetout(&to, &op) > 0) {725if (!start_video && (op.granulepos >= video_granulepos || video_granulepos == (1LL << ti.keyframe_granule_shift))) {726if (op.granulepos > 0) {727current_frame = th_granule_frame(td, op.granulepos);728}729start_video = true;730}731// Don't start decoding until a keyframe is found, but count frames.732if (start_video) {733if (!keyframe_found && th_packet_iskeyframe(&op)) {734keyframe_found = true;735int64_t cur_granulepos = (current_frame + 1) << ti.keyframe_granule_shift;736th_decode_ctl(td, TH_DECCTL_SET_GRANPOS, &cur_granulepos, sizeof(cur_granulepos));737}738if (keyframe_found) {739int64_t videobuf_granulepos;740if (op.granulepos >= 0) {741th_decode_ctl(td, TH_DECCTL_SET_GRANPOS, &op.granulepos, sizeof(op.granulepos));742}743int ret = th_decode_packetin(td, &op, &videobuf_granulepos);744if (ret == 0 || ret == TH_DUPFRAME) {745last_video_time = th_granule_time(td, videobuf_granulepos);746first_frame_decoded = true;747}748} else {749current_frame++;750}751}752}753}754755if (first_frame_decoded) {756if (is_playing()) {757// Draw the current frame.758th_ycbcr_buffer yuv;759th_decode_ycbcr_out(td, yuv);760video_write(yuv);761current_frame_time = last_video_time;762} else {763next_frame_time = current_frame_time;764video_ready = true;765}766}767}768769int VideoStreamPlaybackTheora::get_channels() const {770return vi.channels;771}772773void VideoStreamPlaybackTheora::set_audio_track(int p_idx) {774audio_track = p_idx;775}776777int VideoStreamPlaybackTheora::get_mix_rate() const {778return vi.rate;779}780781VideoStreamPlaybackTheora::VideoStreamPlaybackTheora() {782texture.instantiate();783}784785VideoStreamPlaybackTheora::~VideoStreamPlaybackTheora() {786clear();787}788789void VideoStreamTheora::_bind_methods() {}790791Ref<Resource> ResourceFormatLoaderTheora::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) {792Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);793if (f.is_null()) {794if (r_error) {795*r_error = ERR_CANT_OPEN;796}797return Ref<Resource>();798}799800VideoStreamTheora *stream = memnew(VideoStreamTheora);801stream->set_file(p_path);802803Ref<VideoStreamTheora> ogv_stream = Ref<VideoStreamTheora>(stream);804805if (r_error) {806*r_error = OK;807}808809return ogv_stream;810}811812void ResourceFormatLoaderTheora::get_recognized_extensions(List<String> *p_extensions) const {813p_extensions->push_back("ogv");814}815816bool ResourceFormatLoaderTheora::handles_type(const String &p_type) const {817return ClassDB::is_parent_class(p_type, "VideoStream");818}819820String ResourceFormatLoaderTheora::get_resource_type(const String &p_path) const {821String el = p_path.get_extension().to_lower();822if (el == "ogv") {823return "VideoStreamTheora";824}825return "";826}827828829