/** * Copyright (c) 2006-2016 LOVE Development Team * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. **/ // STL #include // LOVE #include "VideoStream.h" using love::filesystem::File; namespace love { namespace video { namespace theora { VideoStream::VideoStream(love::filesystem::File *file) : file(file) , headerParsed(false) , streamInited(false) , videoSerial(0) , decoder(nullptr) , frameReady(false) , lastFrame(0) , nextFrame(0) , eos(false) , lagCounter(0) { ogg_sync_init(&sync); th_info_init(&videoInfo); frontBuffer = new Frame(); backBuffer = new Frame(); try { parseHeader(); } catch (love::Exception &ex) { delete backBuffer; delete frontBuffer; th_info_clear(&videoInfo); ogg_sync_clear(&sync); throw ex; } frameSync.set(new DeltaSync(), Acquire::NORETAIN); } VideoStream::~VideoStream() { if (decoder) th_decode_free(decoder); th_info_clear(&videoInfo); if (headerParsed) ogg_stream_clear(&stream); ogg_sync_clear(&sync); delete frontBuffer; delete backBuffer; } int VideoStream::getWidth() const { if (headerParsed) return videoInfo.pic_width; else return 0; } int VideoStream::getHeight() const { if (headerParsed) return videoInfo.pic_height; else return 0; } const std::string &VideoStream::getFilename() const { return file->getFilename(); } void VideoStream::setSync(FrameSync *frameSync) { love::thread::Lock l(bufferMutex); this->frameSync = frameSync; } const void *VideoStream::getFrontBuffer() const { return frontBuffer; } size_t VideoStream::getSize() const { return sizeof(Frame); } bool VideoStream::isPlaying() const { return frameSync->isPlaying() && !eos; } void VideoStream::readPage() { char *syncBuffer = nullptr; while (ogg_sync_pageout(&sync, &page) != 1) { if (syncBuffer && !headerParsed && ogg_stream_check(&stream)) throw love::Exception("Invalid stream"); syncBuffer = ogg_sync_buffer(&sync, 8192); size_t read = file->read(syncBuffer, 8192); ogg_sync_wrote(&sync, read); } } bool VideoStream::readPacket(bool mustSucceed) { if (!streamInited) { readPage(); videoSerial = ogg_page_serialno(&page); ogg_stream_init(&stream, videoSerial); streamInited = true; ogg_stream_pagein(&stream, &page); } while (ogg_stream_packetout(&stream, &packet) != 1) { // We need to read another page, but there is none, we're at the end if (ogg_page_eos(&page) && !mustSucceed) return eos = true; do { readPage(); } while (ogg_page_serialno(&page) != videoSerial); ogg_stream_pagein(&stream, &page); } return false; } template inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y) { switch(fmt) { case TH_PF_420: y /= 2; case TH_PF_422: x /= 2; break; default: break; } } void VideoStream::parseHeader() { if (headerParsed) return; th_comment comment; th_setup_info *setupInfo = nullptr; th_comment_init(&comment); int ret; do { readPacket(); ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); if (ret == TH_ENOTFORMAT) { ogg_stream_clear(&stream); streamInited = false; } } while(ret < 0 && !ogg_page_eos(&page)); if (ret < 0) { th_comment_clear(&comment); throw love::Exception("Could not find header"); } while (ret > 0) { readPacket(); ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); } th_comment_clear(&comment); decoder = th_decode_alloc(&videoInfo, setupInfo); th_setup_free(setupInfo); Frame *buffers[2] = {backBuffer, frontBuffer}; yPlaneXOffset = cPlaneXOffset = videoInfo.pic_x; yPlaneYOffset = cPlaneYOffset = videoInfo.pic_y; scaleFormat(videoInfo.pixel_fmt, cPlaneXOffset, cPlaneYOffset); for (int i = 0; i < 2; i++) { buffers[i]->cw = buffers[i]->yw = videoInfo.pic_width; buffers[i]->ch = buffers[i]->yh = videoInfo.pic_height; scaleFormat(videoInfo.pixel_fmt, buffers[i]->cw, buffers[i]->ch); buffers[i]->yplane = new unsigned char[buffers[i]->yw * buffers[i]->yh]; buffers[i]->cbplane = new unsigned char[buffers[i]->cw * buffers[i]->ch]; buffers[i]->crplane = new unsigned char[buffers[i]->cw * buffers[i]->ch]; memset(buffers[i]->yplane, 16, buffers[i]->yw * buffers[i]->yh); memset(buffers[i]->cbplane, 128, buffers[i]->cw * buffers[i]->ch); memset(buffers[i]->crplane, 128, buffers[i]->cw * buffers[i]->ch); } headerParsed = true; th_decode_packetin(decoder, &packet, nullptr); } // Arbitrary seeking isn't supported yet, but rewinding is void VideoStream::rewind() { // Seek our data stream back to the start file->seek(0); // Break our sync, and discard the rest of the page ogg_sync_reset(&sync); ogg_sync_pageseek(&sync, &page); // Read our first page/packet from the stream again readPacket(true); // Now tell theora we're at frame 1 (not 0!) int64 granPos = 1; th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &granPos, sizeof(granPos)); // Force a redraw, since this will always be less than the sync's position lastFrame = nextFrame = -1; eos = false; } void VideoStream::seekDecoder(double target) { if (target < 0.01) { rewind(); return; } double low = 0; double high = file->getSize(); while (high-low > 0.0001) { // Determine our next binary search position double pos = (high+low)/2; file->seek(pos); // Break sync ogg_sync_reset(&sync); ogg_sync_pageseek(&sync, &page); // Read a packet readPacket(false); if (eos) return; // Determine if this is the right place double curTime = th_granule_time(decoder, packet.granulepos); double nextTime = th_granule_time(decoder, packet.granulepos+1); if (curTime == -1) continue; // Invalid granule position (magic?) else if (curTime <= target && nextTime > target) break; // the current frame should be displaying right now else if (curTime > target) high = pos; else low = pos; } // Now update theora and our decoder on this new position of ours lastFrame = nextFrame = -1; eos = false; th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos)); } void VideoStream::threadedFillBackBuffer(double dt) { // Synchronize frameSync->update(dt); double position = frameSync->getPosition(); // Seeking backwards if (position < lastFrame) seekDecoder(position); // If we're at the end of the stream, or if we're displaying the right frame // stop here if (eos || position < nextFrame) return; th_ycbcr_buffer bufferinfo; th_decode_ycbcr_out(decoder, bufferinfo); ogg_int64_t granulePosition; do { if (readPacket()) return; } while (th_decode_packetin(decoder, &packet, &granulePosition) != 0); lastFrame = nextFrame; nextFrame = th_granule_time(decoder, granulePosition); { // Don't swap whilst we're writing to the backbuffer love::thread::Lock l(bufferMutex); frameReady = false; } for (int y = 0; y < backBuffer->yh; ++y) { memcpy(backBuffer->yplane+backBuffer->yw*y, bufferinfo[0].data+ bufferinfo[0].stride*(y+yPlaneYOffset)+yPlaneXOffset, backBuffer->yw); } for (int y = 0; y < backBuffer->ch; ++y) { memcpy(backBuffer->cbplane+backBuffer->cw*y, bufferinfo[1].data+ bufferinfo[1].stride*(y+cPlaneYOffset)+cPlaneXOffset, backBuffer->cw); } for (int y = 0; y < backBuffer->ch; ++y) { memcpy(backBuffer->crplane+backBuffer->cw*y, bufferinfo[2].data+ bufferinfo[2].stride*(y+cPlaneYOffset)+cPlaneXOffset, backBuffer->cw); } // Seeking forwards: // If we're still not on the right frame, either we're lagging or we're seeking // After 5 frames, go for a seek. This is not ideal.. but what is if (position > nextFrame) { if (++lagCounter > 5) seek(position); } else lagCounter = 0; love::thread::Lock l(bufferMutex); frameReady = true; } void VideoStream::fillBackBuffer() { // Done in worker thread } bool VideoStream::swapBuffers() { if (eos) return false; love::thread::Lock l(bufferMutex); if (!frameReady) return false; frameReady = false; Frame *temp = frontBuffer; frontBuffer = backBuffer; backBuffer = temp; return true; } } // theora } // video } // love