1/**
2 * Copyright (c) 2006-2023 LOVE Development Team
3 *
4 * This software is provided 'as-is', without any express or implied
5 * warranty. In no event will the authors be held liable for any damages
6 * arising from the use of this software.
7 *
8 * Permission is granted to anyone to use this software for any purpose,
9 * including commercial applications, and to alter it and redistribute it
10 * freely, subject to the following restrictions:
11 *
12 * 1. The origin of this software must not be misrepresented; you must not
13 * claim that you wrote the original software. If you use this software
14 * in a product, an acknowledgment in the product documentation would be
15 * appreciated but is not required.
16 * 2. Altered source versions must be plainly marked as such, and must not be
17 * misrepresented as being the original software.
18 * 3. This notice may not be removed or altered from any source distribution.
19 **/
20
21// STL
22#include <iostream>
23
24// LOVE
25#include "TheoraVideoStream.h"
26
27using love::filesystem::File;
28
29namespace love
30{
31namespace video
32{
33namespace theora
34{
35
36TheoraVideoStream::TheoraVideoStream(love::filesystem::File *file)
37 : demuxer(file)
38 , headerParsed(false)
39 , decoder(nullptr)
40 , frameReady(false)
41 , lastFrame(0)
42 , nextFrame(0)
43{
44 if (demuxer.findStream() != OggDemuxer::TYPE_THEORA)
45 throw love::Exception("Invalid video file, video is not theora");
46
47 th_info_init(&videoInfo);
48
49 frontBuffer = new Frame();
50 backBuffer = new Frame();
51
52 try
53 {
54 parseHeader();
55 }
56 catch (love::Exception &ex)
57 {
58 delete backBuffer;
59 delete frontBuffer;
60 th_info_clear(&videoInfo);
61 throw ex;
62 }
63
64 frameSync.set(new DeltaSync(), Acquire::NORETAIN);
65}
66
67TheoraVideoStream::~TheoraVideoStream()
68{
69 if (decoder)
70 th_decode_free(decoder);
71
72 th_info_clear(&videoInfo);
73
74 delete frontBuffer;
75 delete backBuffer;
76}
77
78int TheoraVideoStream::getWidth() const
79{
80 if (headerParsed)
81 return videoInfo.pic_width;
82 else
83 return 0;
84}
85
86int TheoraVideoStream::getHeight() const
87{
88 if (headerParsed)
89 return videoInfo.pic_height;
90 else
91 return 0;
92}
93
94const std::string &TheoraVideoStream::getFilename() const
95{
96 return demuxer.getFilename();
97}
98
99void TheoraVideoStream::setSync(FrameSync *frameSync)
100{
101 love::thread::Lock l(bufferMutex);
102 this->frameSync = frameSync;
103}
104
105const void *TheoraVideoStream::getFrontBuffer() const
106{
107 return frontBuffer;
108}
109
110size_t TheoraVideoStream::getSize() const
111{
112 return sizeof(Frame);
113}
114
115bool TheoraVideoStream::isPlaying() const
116{
117 return frameSync->isPlaying() && !demuxer.isEos();
118}
119
120template<typename T>
121inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y)
122{
123 switch(fmt)
124 {
125 case TH_PF_420:
126 y /= 2;
127 case TH_PF_422:
128 x /= 2;
129 break;
130 default:
131 break;
132 }
133}
134
135void TheoraVideoStream::parseHeader()
136{
137 if (headerParsed)
138 return;
139
140 th_comment comment;
141 th_setup_info *setupInfo = nullptr;
142 th_comment_init(&comment);
143 int ret;
144
145 demuxer.readPacket(packet);
146 ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
147
148 if (ret < 0)
149 {
150 th_comment_clear(&comment);
151 throw love::Exception("Could not find header");
152 }
153
154 while (ret > 0)
155 {
156 demuxer.readPacket(packet);
157 ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
158 }
159
160 th_comment_clear(&comment);
161
162 decoder = th_decode_alloc(&videoInfo, setupInfo);
163 th_setup_free(setupInfo);
164
165 Frame *buffers[2] = {backBuffer, frontBuffer};
166
167 yPlaneXOffset = cPlaneXOffset = videoInfo.pic_x;
168 yPlaneYOffset = cPlaneYOffset = videoInfo.pic_y;
169
170 scaleFormat(videoInfo.pixel_fmt, cPlaneXOffset, cPlaneYOffset);
171
172 for (int i = 0; i < 2; i++)
173 {
174 buffers[i]->cw = buffers[i]->yw = videoInfo.pic_width;
175 buffers[i]->ch = buffers[i]->yh = videoInfo.pic_height;
176
177 scaleFormat(videoInfo.pixel_fmt, buffers[i]->cw, buffers[i]->ch);
178
179 buffers[i]->yplane = new unsigned char[buffers[i]->yw * buffers[i]->yh];
180 buffers[i]->cbplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];
181 buffers[i]->crplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];
182
183 memset(buffers[i]->yplane, 16, buffers[i]->yw * buffers[i]->yh);
184 memset(buffers[i]->cbplane, 128, buffers[i]->cw * buffers[i]->ch);
185 memset(buffers[i]->crplane, 128, buffers[i]->cw * buffers[i]->ch);
186 }
187
188 headerParsed = true;
189 th_decode_packetin(decoder, &packet, nullptr);
190}
191
192void TheoraVideoStream::seekDecoder(double target)
193{
194 bool success = demuxer.seek(packet, target, [this](int64 granulepos) {
195 return th_granule_time(decoder, granulepos);
196 });
197
198 if (!success)
199 return;
200
201 // Now update theora and our decoder on this new position of ours
202 lastFrame = nextFrame = -1;
203 th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos));
204}
205
206void TheoraVideoStream::threadedFillBackBuffer(double dt)
207{
208 // Synchronize
209 frameSync->update(dt);
210 double position = frameSync->getPosition();
211
212 // Seeking backwards
213 if (position < lastFrame)
214 seekDecoder(position);
215
216 th_ycbcr_buffer bufferinfo;
217 bool hasFrame = false;
218
219 // Until we are at the end of the stream, or we are displaying the right frame
220 unsigned int framesBehind = 0;
221 bool failedSeek = false;
222 while (!demuxer.isEos() && position >= nextFrame)
223 {
224 // If we can't catch up, seek
225 if (framesBehind++ > 5 && !failedSeek)
226 {
227 seekDecoder(position);
228 framesBehind = 0;
229 failedSeek = true;
230 }
231
232 th_decode_ycbcr_out(decoder, bufferinfo);
233 hasFrame = true;
234
235 ogg_int64_t granulePosition;
236 do
237 {
238 if (demuxer.readPacket(packet))
239 return;
240 } while (th_decode_packetin(decoder, &packet, &granulePosition) != 0);
241 lastFrame = nextFrame;
242 nextFrame = th_granule_time(decoder, granulePosition);
243 }
244
245 // Only swap once, even if we read many frames to get here
246 if (hasFrame)
247 {
248 // Don't swap whilst we're writing to the backbuffer
249 {
250 love::thread::Lock l(bufferMutex);
251 frameReady = false;
252 }
253
254 for (int y = 0; y < backBuffer->yh; ++y)
255 {
256 memcpy(backBuffer->yplane+backBuffer->yw*y,
257 bufferinfo[0].data+
258 bufferinfo[0].stride*(y+yPlaneYOffset)+yPlaneXOffset,
259 backBuffer->yw);
260 }
261
262 for (int y = 0; y < backBuffer->ch; ++y)
263 {
264 memcpy(backBuffer->cbplane+backBuffer->cw*y,
265 bufferinfo[1].data+
266 bufferinfo[1].stride*(y+cPlaneYOffset)+cPlaneXOffset,
267 backBuffer->cw);
268 }
269
270 for (int y = 0; y < backBuffer->ch; ++y)
271 {
272 memcpy(backBuffer->crplane+backBuffer->cw*y,
273 bufferinfo[2].data+
274 bufferinfo[2].stride*(y+cPlaneYOffset)+cPlaneXOffset,
275 backBuffer->cw);
276 }
277
278 // Re-enable swapping
279 {
280 love::thread::Lock l(bufferMutex);
281 frameReady = true;
282 }
283 }
284}
285
286void TheoraVideoStream::fillBackBuffer()
287{
288 // Done in worker thread
289}
290
291bool TheoraVideoStream::swapBuffers()
292{
293 if (demuxer.isEos())
294 return false;
295
296 if (!frameSync->isPlaying())
297 return false;
298
299 love::thread::Lock l(bufferMutex);
300 if (!frameReady)
301 return false;
302 frameReady = false;
303
304 Frame *temp = frontBuffer;
305 frontBuffer = backBuffer;
306 backBuffer = temp;
307
308 return true;
309}
310
311} // theora
312} // video
313} // love
314