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#include "OggDemuxer.h"
22
23namespace love
24{
25namespace video
26{
27namespace theora
28{
29
30OggDemuxer::OggDemuxer(love::filesystem::File *file)
31 : file(file)
32 , streamInited(false)
33 , videoSerial(0)
34 , eos(false)
35{
36 ogg_sync_init(&sync);
37}
38
39OggDemuxer::~OggDemuxer()
40{
41 if (streamInited)
42 ogg_stream_clear(&stream);
43 ogg_sync_clear(&sync);
44}
45
46bool OggDemuxer::readPage(bool erroreof)
47{
48 char *syncBuffer = nullptr;
49 while (ogg_sync_pageout(&sync, &page) != 1)
50 {
51 if (syncBuffer && !streamInited && ogg_stream_check(&stream))
52 throw love::Exception("Invalid stream");
53
54 syncBuffer = ogg_sync_buffer(&sync, 8192);
55 size_t read = file->read(syncBuffer, 8192);
56 if (read == 0 && erroreof)
57 return false;
58
59 ogg_sync_wrote(&sync, read);
60 }
61
62 return true;
63}
64
65bool OggDemuxer::readPacket(ogg_packet &packet, bool mustSucceed)
66{
67 if (!streamInited)
68 throw love::Exception("Reading from OggDemuxer before initialization (engine bug)");
69
70 while (ogg_stream_packetout(&stream, &packet) != 1)
71 {
72 do
73 {
74 // We need to read another page, but there is none, we're at the end
75 if (ogg_page_serialno(&page) == videoSerial && ogg_page_eos(&page) && !mustSucceed)
76 return eos = true;
77
78 readPage();
79 } while (ogg_page_serialno(&page) != videoSerial);
80
81 ogg_stream_pagein(&stream, &page);
82 }
83
84 return eos = false;
85}
86
87void OggDemuxer::resync()
88{
89 ogg_sync_reset(&sync);
90 ogg_sync_pageseek(&sync, &page);
91 ogg_stream_reset(&stream);
92}
93
94bool OggDemuxer::isEos() const
95{
96 return eos;
97}
98
99const std::string &OggDemuxer::getFilename() const
100{
101 return file->getFilename();
102}
103
104OggDemuxer::StreamType OggDemuxer::determineType()
105{
106 ogg_packet packet;
107 if (ogg_stream_packetpeek(&stream, &packet) != 1)
108 return TYPE_UNKNOWN;
109
110 // Theora
111 // See https://www.theora.org/doc/Theora.pdf section 6.1
112 if (packet.bytes >= 7) {
113 uint8_t headerType = packet.packet[0];
114 if (headerType & 0x80 && std::strncmp((const char*) packet.packet+1, "theora", 6) == 0)
115 return TYPE_THEORA;
116 }
117
118 return TYPE_UNKNOWN;
119}
120
121OggDemuxer::StreamType OggDemuxer::findStream()
122{
123 if (streamInited)
124 {
125 eos = false;
126 streamInited = false;
127 file->seek(0);
128 ogg_stream_clear(&stream);
129 ogg_sync_reset(&sync);
130 }
131
132 while (true)
133 {
134 if (!readPage(true))
135 return TYPE_UNKNOWN;
136
137 // If this page isn't at the start of a stream, we've seen all streams
138 if (!ogg_page_bos(&page))
139 break;
140
141 videoSerial = ogg_page_serialno(&page);
142 ogg_stream_init(&stream, videoSerial);
143 ogg_stream_pagein(&stream, &page);
144 streamInited = true;
145
146 StreamType type = determineType();
147 switch(type)
148 {
149 case TYPE_THEORA:
150 return type;
151 default:
152 break;
153 }
154
155 ogg_stream_clear(&stream);
156 streamInited = false;
157 }
158
159 if (streamInited)
160 {
161 streamInited = false;
162 ogg_stream_clear(&stream);
163 }
164
165 ogg_sync_reset(&sync);
166
167 return TYPE_UNKNOWN;
168}
169
170bool OggDemuxer::seek(ogg_packet &packet, double target, std::function<double(int64)> getTime)
171{
172 static const double rewindThreshold = 0.01;
173
174 eos = false;
175
176 if (target < rewindThreshold)
177 {
178 file->seek(0);
179 resync();
180 readPacket(packet, true);
181 return true;
182 }
183
184 double low = 0;
185 double high = file->getSize();
186
187 // If we know our current position, we can drastically decrease the search area
188 if (packet.granulepos != -1)
189 {
190 double currentTime = getTime(packet.granulepos);
191 if (currentTime < target)
192 low = file->tell();
193 else if (currentTime > target)
194 high = file->tell();
195 }
196
197 while (high-low > rewindThreshold)
198 {
199 // Determine our next binary search position
200 double pos = (high+low)/2;
201 file->seek(pos);
202
203 // Break sync
204 resync();
205
206 // Read a page
207 readPage();
208 readPacket(packet, false);
209 if (isEos())
210 {
211 // EOS, so we're definitely past our target (or the target is past
212 // the end)
213 high = pos;
214 eos = false;
215
216 // And a workaround for single-page files:
217 if (high < rewindThreshold)
218 {
219 file->seek(0);
220 resync();
221 readPacket(packet, true);
222 }
223 else
224 continue;
225 }
226
227 // Now search all packets in this page
228 int result = -1;
229 for (int i = 0; i < ogg_page_packets(&page); ++i)
230 {
231 if (i > 0)
232 readPacket(packet, true);
233
234 // Determine if this is the right place
235 double curTime = getTime(packet.granulepos);
236 double nextTime = getTime(packet.granulepos+1);
237
238 if (curTime == -1)
239 continue; // Invalid granule position (magic?)
240 else if (curTime <= target && nextTime > target)
241 {
242 // the current frame should be displaying right now
243 result = 0;
244 break;
245 }
246 else if (curTime > target)
247 {
248 // No need to check the other packets, they're all past
249 // this one
250 result = 1;
251 break;
252 }
253 }
254
255 // The sign of result determines the direction
256 if (result == 0)
257 break;
258 else if (result < 0)
259 low = pos;
260 else
261 high = pos;
262 }
263
264 return true;
265}
266
267} // theora
268} // video
269} // love
270