Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
VideoRuntime.cpp
Go to the documentation of this file.
1#include "VideoRuntime.hpp"
2
3#include <algorithm>
4#include <array>
5#include <cstring>
6#include <limits>
7
8namespace monopoly::video
9{
10 namespace
11 {
12 [[nodiscard]] std::uint32_t u32le(
13 std::span<const std::uint8_t> bytes,
14 std::size_t offset)
15 {
16 return static_cast<std::uint32_t>(bytes[offset]) |
17 (static_cast<std::uint32_t>(bytes[offset + 1]) << 8U) |
18 (static_cast<std::uint32_t>(bytes[offset + 2]) << 16U) |
19 (static_cast<std::uint32_t>(bytes[offset + 3]) << 24U);
20 }
21
22 [[nodiscard]] bool fourcc(
23 std::span<const std::uint8_t> bytes,
24 std::size_t offset,
25 const char (&id)[5])
26 {
27 return offset + 4 <= bytes.size() &&
28 std::memcmp(bytes.data() + offset, id, 4) == 0;
29 }
30
31 [[nodiscard]] std::expected<
32 std::span<const std::uint8_t>, std::string>
34 std::span<const std::uint8_t> bytes,
35 const char (&wanted)[5])
36 {
37 std::size_t offset{};
38 while (offset + 8 <= bytes.size())
39 {
40 const auto size =
41 static_cast<std::size_t>(u32le(bytes, offset + 4));
42 const auto payload = offset + 8;
43 if (payload + size > bytes.size())
44 return std::unexpected(
45 "AVI chunk exceeds containing RIFF data");
46
48 return bytes.subspan(payload, size);
49
50 if ((fourcc(bytes, offset, "RIFF") ||
51 fourcc(bytes, offset, "LIST")) &&
52 size >= 4)
53 {
54 const auto nested = bytes.subspan(
55 payload + 4, size - 4);
57 if (found) return found;
58 }
59
60 offset = payload + size + (size & 1U);
61 }
62 return std::unexpected("AVI chunk not found");
63 }
64
65 [[nodiscard]] bool validFrame(
66 const Status& status,
67 std::int32_t frame) noexcept
68 {
69 return frame >= 0 &&
70 frame < status.numberOfFrames;
71 }
72 }
73
74 std::expected<AviMetadata, std::string>
75 parseAviMetadata(std::span<const std::uint8_t> bytes)
76 {
77 if (bytes.size() < 12 ||
78 !fourcc(bytes, 0, "RIFF") ||
79 !fourcc(bytes, 8, "AVI "))
80 return std::unexpected(
81 "video is not a RIFF AVI stream");
82
83 const auto declared =
84 static_cast<std::size_t>(u32le(bytes, 4));
85 if (declared + 8 > bytes.size())
86 return std::unexpected(
87 "AVI RIFF size exceeds supplied data");
88
89 const auto root =
90 bytes.subspan(12, declared >= 4 ? declared - 4 : 0);
91 const auto avih = findChunk(root, "avih");
92 if (!avih) return std::unexpected(avih.error());
93 if (avih->size() < 40)
94 return std::unexpected(
95 "AVI main header is truncated");
96
97 AviMetadata result{};
98 result.microsecondsPerFrame = u32le(*avih, 0);
99 result.totalFrames = u32le(*avih, 16);
100 result.width = u32le(*avih, 32);
101 result.height = u32le(*avih, 36);
102
103 if (result.microsecondsPerFrame == 0)
104 return std::unexpected(
105 "AVI main header has zero frame duration");
106 if (result.totalFrames == 0)
107 return std::unexpected(
108 "AVI main header has zero frames");
109 if (result.width == 0 || result.height == 0)
110 return std::unexpected(
111 "AVI main header has zero dimensions");
112 return result;
113 }
114
115 std::expected<void, std::string> Runtime::open(
116 std::span<const std::uint8_t> aviBytes,
117 bool loopAtEnd)
118 {
119 const auto parsed = parseAviMetadata(aviBytes);
120 if (!parsed) return std::unexpected(parsed.error());
121
122 return open(*parsed, loopAtEnd);
123 }
124
125 std::expected<void, std::string> Runtime::open(
127 {
130 static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max()))
131 return std::unexpected("video metadata is outside runtime limits");
132 metadata_ = metadata;
133 status_.currentFrame = -1;
134 status_.desiredFrame = -1;
135 status_.numberOfFrames =
136 static_cast<std::int32_t>(metadata_.totalFrames);
137 status_.ended = false;
138 loopAtEnd_ = loopAtEnd;
139 alternatives_.clear();
140 pendingJump_.reset();
141 return {};
142 }
143
145 {
146 metadata_ = {};
147 status_ = {};
148 status_.currentFrame = -1;
149 status_.desiredFrame = -1;
150 loopAtEnd_ = false;
151 alternatives_.clear();
152 pendingJump_.reset();
153 }
154
155 std::expected<Status, std::string> Runtime::feedToFrame(
156 std::int32_t desiredFrame)
157 {
158 if (!open())
159 return std::unexpected(
160 "video runtime is not open");
161 if (desiredFrame < 0)
162 return std::unexpected(
163 "desired video frame is negative");
164
165 if (desiredFrame >= status_.numberOfFrames)
166 {
167 if (loopAtEnd_)
168 desiredFrame %= status_.numberOfFrames;
169 else
170 {
171 status_.desiredFrame = desiredFrame;
172 status_.currentFrame = status_.numberOfFrames;
173 status_.ended = true;
174 return status_;
175 }
176 }
177
178 status_.desiredFrame = desiredFrame;
179 status_.ended = false;
180
181 const auto begin =
182 std::max(status_.currentFrame + 1, 0);
183 const auto decision = alternatives_.lower_bound(begin);
184 if (decision != alternatives_.end() &&
185 decision->first <= desiredFrame)
186 {
187 pendingJump_ = JumpEvent{
188 decision->first,
189 decision->second.jumpToFrame,
190 decision->second.takeJump};
191
192 if (decision->second.takeJump)
193 {
194 if (!validFrame(status_, decision->second.jumpToFrame))
195 return std::unexpected(
196 "video alternative jump target is out of range");
197 status_.currentFrame = decision->second.jumpToFrame;
198 status_.desiredFrame = decision->second.jumpToFrame;
199 return status_;
200 }
201
202 // The retail player always shows the decision frame before
203 // continuing on the normal path and emits UIMSG_VIDEO_JUMP there.
204 status_.currentFrame = decision->first;
205 return status_;
206 }
207
208 status_.currentFrame = desiredFrame;
209 return status_;
210 }
211
212 std::expected<std::int32_t, std::string>
214 std::uint64_t elapsedMicroseconds) const
215 {
216 if (!open())
217 return std::unexpected(
218 "video runtime is not open");
219 const auto frame =
220 elapsedMicroseconds / metadata_.microsecondsPerFrame;
221 if (frame >
222 static_cast<std::uint64_t>(
223 std::numeric_limits<std::int32_t>::max()))
224 return std::unexpected(
225 "elapsed video time exceeds runtime range");
226 return static_cast<std::int32_t>(frame);
227 }
228
229 std::expected<void, std::string> Runtime::cutToFrame(
230 std::int32_t frame)
231 {
232 if (!open())
233 return std::unexpected(
234 "video runtime is not open");
235 if (!validFrame(status_, frame))
236 return std::unexpected(
237 "video cut target is out of range");
238 status_.currentFrame = frame;
239 status_.desiredFrame = frame;
240 status_.ended = false;
241 return {};
242 }
243
244 std::expected<void, std::string> Runtime::setAlternative(
245 std::int32_t decisionFrame,
246 std::int32_t jumpToFrame)
247 {
248 if (!open())
249 return std::unexpected(
250 "video runtime is not open");
251 if (!validFrame(status_, decisionFrame))
252 return std::unexpected(
253 "video alternative decision frame is out of range");
254 if (jumpToFrame < 0)
255 {
256 alternatives_.erase(decisionFrame);
257 return {};
258 }
259 if (!validFrame(status_, jumpToFrame))
260 return std::unexpected(
261 "video alternative jump frame is out of range");
262
263 alternatives_[decisionFrame] =
264 Alternative{jumpToFrame, false};
265 return {};
266 }
267
269 std::int32_t decisionFrame,
270 bool takeJump) noexcept
271 {
272 const auto found =
273 alternatives_.find(decisionFrame);
274 if (found == alternatives_.end())
275 return false;
276 found->second.takeJump = takeJump;
277 return true;
278 }
279
281 {
282 alternatives_.clear();
283 }
284
285 std::optional<JumpEvent> Runtime::takeJumpEvent() noexcept
286 {
287 auto result = pendingJump_;
288 pendingJump_.reset();
289 return result;
290 }
291}
std::size_t offset
std::size_t root
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
unsigned index
std::size_t frame
const AviMetadata & metadata() const noexcept
std::expected< void, std::string > cutToFrame(std::int32_t frame)
std::expected< std::int32_t, std::string > frameAtElapsed(std::uint64_t elapsedMicroseconds) const
std::expected< Status, std::string > feedToFrame(std::int32_t desiredFrame)
std::optional< JumpEvent > takeJumpEvent() noexcept
bool open() const noexcept
std::expected< void, std::string > setAlternative(std::int32_t decisionFrame, std::int32_t jumpToFrame)
void forgetAlternatives() noexcept
bool changeAlternative(std::int32_t decisionFrame, bool takeJump) noexcept
std::size_t size()
std::expected< AviMetadata, std::string > parseAviMetadata(std::span< const std::uint8_t > bytes)