Files
2026-10-29 03:55:40 +00:00

597 lines
25 KiB
C++

#include "export.hpp"
#include <SDL.h>
#include <SDL_image.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cmath>
#include <fstream>
#include <string>
#include <unordered_map>
#include <vector>
namespace sppm {
namespace {
using Bytes = std::vector<std::uint8_t>;
void be32(Bytes& out, std::uint32_t value) {
out.push_back(static_cast<std::uint8_t>(value >> 24));
out.push_back(static_cast<std::uint8_t>(value >> 16));
out.push_back(static_cast<std::uint8_t>(value >> 8));
out.push_back(static_cast<std::uint8_t>(value));
}
void be16(Bytes& out, std::uint16_t value) {
out.push_back(static_cast<std::uint8_t>(value >> 8));
out.push_back(static_cast<std::uint8_t>(value));
}
void type(Bytes& out, const char* value) {
out.insert(out.end(), value, value + 4);
}
std::size_t boxBegin(Bytes& out, const char* name) {
const std::size_t start = out.size();
be32(out, 0); type(out, name);
return start;
}
void boxEnd(Bytes& out, std::size_t start) {
const std::size_t size = out.size() - start;
out[start] = static_cast<std::uint8_t>(size >> 24);
out[start + 1] = static_cast<std::uint8_t>(size >> 16);
out[start + 2] = static_cast<std::uint8_t>(size >> 8);
out[start + 3] = static_cast<std::uint8_t>(size);
}
std::size_t fullBoxBegin(Bytes& out, const char* name, std::uint8_t version = 0,
std::uint32_t flags = 0) {
const auto start = boxBegin(out, name);
out.push_back(version);
out.push_back(static_cast<std::uint8_t>(flags >> 16));
out.push_back(static_cast<std::uint8_t>(flags >> 8));
out.push_back(static_cast<std::uint8_t>(flags));
return start;
}
Bytes mp4MovieBox(const std::vector<std::uint32_t>& sizes,
const std::vector<std::uint32_t>& offsets,
double fps) {
constexpr std::uint32_t timeScale = 1000000;
const std::uint32_t frameDelta = static_cast<std::uint32_t>(std::lround(timeScale / fps));
const std::uint32_t duration = static_cast<std::uint32_t>(std::llround(sizes.size() * static_cast<double>(timeScale) / fps));
Bytes out;
auto moov = boxBegin(out, "moov");
auto mvhd = fullBoxBegin(out, "mvhd");
be32(out, 0); be32(out, 0); be32(out, timeScale);
be32(out, duration);
be32(out, 0x00010000); be16(out, 0x0100); be16(out, 0);
be32(out, 0); be32(out, 0);
const std::uint32_t matrix[] = {0x00010000,0,0, 0,0x00010000,0, 0,0,0x40000000};
for (auto value : matrix) be32(out, value);
for (int i = 0; i < 6; ++i) be32(out, 0);
be32(out, 2); boxEnd(out, mvhd);
auto trak = boxBegin(out, "trak");
auto tkhd = fullBoxBegin(out, "tkhd", 0, 7);
be32(out, 0); be32(out, 0); be32(out, 1); be32(out, 0);
be32(out, duration);
be32(out, 0); be32(out, 0); be16(out, 0); be16(out, 0); be16(out, 0); be16(out, 0);
for (auto value : matrix) be32(out, value);
be32(out, static_cast<std::uint32_t>(kWidth) << 16);
be32(out, static_cast<std::uint32_t>(kHeight) << 16); boxEnd(out, tkhd);
auto mdia = boxBegin(out, "mdia");
auto mdhd = fullBoxBegin(out, "mdhd");
be32(out, 0); be32(out, 0); be32(out, timeScale);
be32(out, duration); be16(out, 0x55c4); be16(out, 0);
boxEnd(out, mdhd);
auto hdlr = fullBoxBegin(out, "hdlr");
be32(out, 0); type(out, "vide"); be32(out, 0); be32(out, 0); be32(out, 0);
out.insert(out.end(), {'S','w','i','t','c','h','n','o','t','e',' ','V','i','d','e','o',0});
boxEnd(out, hdlr);
auto minf = boxBegin(out, "minf");
auto vmhd = fullBoxBegin(out, "vmhd", 0, 1);
be16(out, 0); be16(out, 0); be16(out, 0); be16(out, 0); boxEnd(out, vmhd);
auto dinf = boxBegin(out, "dinf");
auto dref = fullBoxBegin(out, "dref"); be32(out, 1);
auto url = fullBoxBegin(out, "url ", 0, 1); boxEnd(out, url); boxEnd(out, dref); boxEnd(out, dinf);
auto stbl = boxBegin(out, "stbl");
auto stsd = fullBoxBegin(out, "stsd"); be32(out, 1);
auto jpeg = boxBegin(out, "mjpg");
be32(out, 0); be16(out, 0); be16(out, 1);
be16(out, 0); be16(out, 0); be32(out, 0); be32(out, 0); be32(out, 0);
be16(out, kWidth); be16(out, kHeight); be32(out, 0x00480000); be32(out, 0x00480000);
be32(out, 0); be16(out, 1); out.insert(out.end(), 32, 0); be16(out, 0x0018); be16(out, 0xffff);
boxEnd(out, jpeg); boxEnd(out, stsd);
auto stts = fullBoxBegin(out, "stts"); be32(out, 1); be32(out, static_cast<std::uint32_t>(sizes.size())); be32(out, frameDelta); boxEnd(out, stts);
auto stsc = fullBoxBegin(out, "stsc"); be32(out, 1); be32(out, 1); be32(out, 1); be32(out, 1); boxEnd(out, stsc);
auto stsz = fullBoxBegin(out, "stsz"); be32(out, 0); be32(out, static_cast<std::uint32_t>(sizes.size()));
for (auto size : sizes) be32(out, size);
boxEnd(out, stsz);
auto stco = fullBoxBegin(out, "stco"); be32(out, static_cast<std::uint32_t>(offsets.size()));
for (auto offset : offsets) be32(out, offset);
boxEnd(out, stco);
auto stss = fullBoxBegin(out, "stss"); be32(out, static_cast<std::uint32_t>(sizes.size()));
for (std::size_t i = 0; i < sizes.size(); ++i) be32(out, static_cast<std::uint32_t>(i + 1));
boxEnd(out, stss);
boxEnd(out, stbl); boxEnd(out, minf); boxEnd(out, mdia); boxEnd(out, trak); boxEnd(out, moov);
return out;
}
Color opaqueFramePixel(const Frame& frame, const Palette& palette, std::size_t pixel) {
Color result{255, 255, 255, 255};
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
const auto index = frame.layers[layerIndex][pixel];
if (!index) continue;
const auto& source = palette[index];
if (!source.a) continue;
const unsigned alpha = source.a;
result.r = static_cast<std::uint8_t>((source.r * alpha + result.r * (255 - alpha) + 127) / 255);
result.g = static_cast<std::uint8_t>((source.g * alpha + result.g * (255 - alpha) + 127) / 255);
result.b = static_cast<std::uint8_t>((source.b * alpha + result.b * (255 - alpha) + 127) / 255);
}
return result;
}
bool checkDocument(const Document& document, bool cover, std::string& error) {
if (document.frames.empty() || document.frames.size() > kMaxFrames) {
error = "frame count must be between 1 and 9999";
return false;
}
if (document.metadata.fps < 1 || document.metadata.fps > 30) {
error = "frame rate must be between 1 and 30 fps";
return false;
}
if (cover && document.metadata.coverFrame >= document.frames.size()) {
error = "cover frame is out of range";
return false;
}
for (const Frame& frame : document.frames) {
for (const auto& layer : frame.layers) {
if (layer.size() != kPixelsPerLayer) {
error = "a layer has the wrong pixel count";
return false;
}
}
}
error.clear();
return true;
}
// Straight-alpha source-over compositing, with A foremost and D rearmost.
Color compositePixel(const Frame& frame, const Palette& palette,
std::size_t pixel) {
Color result{0, 0, 0, 0};
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
const auto& layer = frame.layers[layerIndex];
const std::uint8_t index = layer[pixel];
if (index == 0) continue;
const Color& source = palette[index];
if (source.a == 0) continue;
if (source.a == 255) {
result = source;
continue;
}
const std::uint32_t sourceWeight =
static_cast<std::uint32_t>(source.a) * 255U;
const std::uint32_t behindWeight =
static_cast<std::uint32_t>(result.a) * (255U - source.a);
const std::uint32_t total = sourceWeight + behindWeight;
result.r = static_cast<std::uint8_t>(
(source.r * sourceWeight + result.r * behindWeight + total / 2U) / total);
result.g = static_cast<std::uint8_t>(
(source.g * sourceWeight + result.g * behindWeight + total / 2U) / total);
result.b = static_cast<std::uint8_t>(
(source.b * sourceWeight + result.b * behindWeight + total / 2U) / total);
result.a = static_cast<std::uint8_t>((total + 127U) / 255U);
}
return result;
}
std::uint8_t nearestPaletteIndex(const Color& color, const Palette& palette) {
std::uint32_t bestDistance = 3U * 255U * 255U + 1U;
std::uint8_t best = 0;
for (std::size_t i = 1; i < palette.size(); ++i) {
const Color& candidate = palette[i];
if (candidate.a == 0) continue;
const int dr = static_cast<int>(color.r) - candidate.r;
const int dg = static_cast<int>(color.g) - candidate.g;
const int db = static_cast<int>(color.b) - candidate.b;
const std::uint32_t distance =
static_cast<std::uint32_t>(dr * dr + dg * dg + db * db);
if (distance < bestDistance) {
bestDistance = distance;
best = static_cast<std::uint8_t>(i);
if (distance == 0) break;
}
}
return best;
}
// GIF has a single transparent index, so partially transparent colors are
// composited first, then matched to the nearest RGB color in the palette.
std::vector<std::uint8_t> gifPixels(const Frame& frame,
const Palette& palette) {
std::vector<std::uint8_t> pixels(kPixelsPerLayer, 0);
bool hasPartialAlpha = false;
for (std::size_t i = 1; i < palette.size(); ++i) {
if (palette[i].a != 0 && palette[i].a != 255) {
hasPartialAlpha = true;
break;
}
}
if (!hasPartialAlpha) {
for (std::size_t layerIndex = frame.layers.size(); layerIndex-- > 0;) {
const auto& layer = frame.layers[layerIndex];
for (std::size_t pixel = 0; pixel < pixels.size(); ++pixel) {
const std::uint8_t index = layer[pixel];
if (index != 0 && palette[index].a != 0) pixels[pixel] = index;
}
}
return pixels;
}
std::unordered_map<std::uint32_t, std::uint8_t> matchCache;
matchCache.reserve(512);
for (std::size_t pixel = 0; pixel < pixels.size(); ++pixel) {
const Color color = compositePixel(frame, palette, pixel);
if (color.a < 128) continue;
const std::uint32_t key =
(static_cast<std::uint32_t>(color.r) << 16U) |
(static_cast<std::uint32_t>(color.g) << 8U) | color.b;
const auto found = matchCache.find(key);
if (found != matchCache.end()) {
pixels[pixel] = found->second;
} else {
const std::uint8_t index = nearestPaletteIndex(color, palette);
matchCache.emplace(key, index);
pixels[pixel] = index;
}
}
return pixels;
}
struct BitPacker {
std::vector<std::uint8_t> data;
std::uint32_t pending = 0;
unsigned pendingBits = 0;
void code(std::uint16_t value, unsigned width) {
pending |= static_cast<std::uint32_t>(value) << pendingBits;
pendingBits += width;
while (pendingBits >= 8) {
data.push_back(static_cast<std::uint8_t>(pending & 0xFFU));
pending >>= 8U;
pendingBits -= 8U;
}
}
void finish() {
if (pendingBits != 0) data.push_back(static_cast<std::uint8_t>(pending));
}
};
std::vector<std::uint8_t> gifLzw(const std::vector<std::uint8_t>& pixels) {
constexpr std::uint16_t clearCode = 256;
constexpr std::uint16_t endCode = 257;
std::unordered_map<std::uint32_t, std::uint16_t> dictionary;
dictionary.reserve(4096);
std::uint16_t nextCode = 258;
unsigned codeWidth = 9;
BitPacker writer;
writer.data.reserve(pixels.size() / 2);
writer.code(clearCode, codeWidth);
std::uint16_t prefix = pixels.front();
for (std::size_t i = 1; i < pixels.size(); ++i) {
const std::uint8_t suffix = pixels[i];
const std::uint32_t key =
(static_cast<std::uint32_t>(prefix) << 8U) | suffix;
const auto found = dictionary.find(key);
if (found != dictionary.end()) {
prefix = found->second;
continue;
}
writer.code(prefix, codeWidth);
if (nextCode < 4096) {
dictionary.emplace(key, nextCode++);
// GIF's decoder learns each new phrase one code later.
if (codeWidth < 12 && nextCode > (1U << codeWidth)) ++codeWidth;
} else {
writer.code(clearCode, codeWidth);
dictionary.clear();
nextCode = 258;
codeWidth = 9;
}
prefix = suffix;
}
writer.code(prefix, codeWidth);
writer.code(endCode, codeWidth);
writer.finish();
return writer.data;
}
void byte(std::ostream& out, std::uint8_t value) {
out.put(static_cast<char>(value));
}
void le16(std::ostream& out, std::uint16_t value) {
byte(out, static_cast<std::uint8_t>(value));
byte(out, static_cast<std::uint8_t>(value >> 8U));
}
void be32(std::ostream& out, std::uint32_t value) {
byte(out, static_cast<std::uint8_t>(value >> 24U));
byte(out, static_cast<std::uint8_t>(value >> 16U));
byte(out, static_cast<std::uint8_t>(value >> 8U));
byte(out, static_cast<std::uint8_t>(value));
}
std::uint32_t crc32(const char* type, const std::vector<std::uint8_t>& data) {
std::uint32_t crc = 0xFFFFFFFFU;
auto update = [&crc](std::uint8_t value) {
crc ^= value;
for (unsigned bit = 0; bit < 8; ++bit) {
crc = (crc >> 1U) ^ ((crc & 1U) ? 0xEDB88320U : 0U);
}
};
for (int i = 0; i < 4; ++i) update(static_cast<std::uint8_t>(type[i]));
for (std::uint8_t value : data) update(value);
return crc ^ 0xFFFFFFFFU;
}
void pngChunk(std::ostream& out, const char* type,
const std::vector<std::uint8_t>& data) {
be32(out, static_cast<std::uint32_t>(data.size()));
out.write(type, 4);
if (!data.empty()) {
out.write(reinterpret_cast<const char*>(data.data()),
static_cast<std::streamsize>(data.size()));
}
be32(out, crc32(type, data));
}
std::vector<std::uint8_t> zlibStored(const std::vector<std::uint8_t>& raw) {
std::vector<std::uint8_t> encoded;
encoded.reserve(raw.size() + 6 + (raw.size() / 65535 + 1) * 5);
encoded.push_back(0x78); // zlib CMF: deflate, 32 KiB window
encoded.push_back(0x01); // zlib FLG: no compression
std::size_t offset = 0;
while (offset < raw.size()) {
const std::size_t length = std::min<std::size_t>(65535, raw.size() - offset);
const bool final = offset + length == raw.size();
const std::uint16_t len = static_cast<std::uint16_t>(length);
const std::uint16_t inverse = static_cast<std::uint16_t>(~len);
encoded.push_back(final ? 0x01 : 0x00);
encoded.push_back(static_cast<std::uint8_t>(len));
encoded.push_back(static_cast<std::uint8_t>(len >> 8U));
encoded.push_back(static_cast<std::uint8_t>(inverse));
encoded.push_back(static_cast<std::uint8_t>(inverse >> 8U));
encoded.insert(encoded.end(), raw.begin() + static_cast<std::ptrdiff_t>(offset),
raw.begin() + static_cast<std::ptrdiff_t>(offset + length));
offset += length;
}
std::uint32_t a = 1;
std::uint32_t b = 0;
for (std::uint8_t value : raw) {
a = (a + value) % 65521U;
b = (b + a) % 65521U;
}
const std::uint32_t adler = (b << 16U) | a;
encoded.push_back(static_cast<std::uint8_t>(adler >> 24U));
encoded.push_back(static_cast<std::uint8_t>(adler >> 16U));
encoded.push_back(static_cast<std::uint8_t>(adler >> 8U));
encoded.push_back(static_cast<std::uint8_t>(adler));
return encoded;
}
} // namespace
bool exportGif(const Document& document, const std::string& path,
std::string& error) {
if (!checkDocument(document, false, error)) return false;
if (path.empty()) {
error = "GIF output path is empty";
return false;
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) {
error = "could not open GIF output";
return false;
}
out.write("GIF89a", 6);
le16(out, kWidth);
le16(out, kHeight);
byte(out, 0xF7); // global 256-color table, 8-bit color resolution
byte(out, 1); // white in the default palette as the canvas background
byte(out, 0);
for (const Color& color : document.palette) {
byte(out, color.r);
byte(out, color.g);
byte(out, color.b);
}
// Netscape application extension: loop indefinitely.
const std::array<std::uint8_t, 19> loop = {
0x21, 0xFF, 0x0B, 'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0',
0x03, 0x01, 0x00, 0x00, 0x00};
out.write(reinterpret_cast<const char*>(loop.data()),
static_cast<std::streamsize>(loop.size()));
for (std::size_t i = 0; i < document.frames.size(); ++i) {
const double fps = frameRate(document.metadata);
const unsigned start = static_cast<unsigned>(std::llround(i * 100.0 / fps));
const unsigned end = static_cast<unsigned>(std::llround((i + 1U) * 100.0 / fps));
const std::uint16_t delay =
static_cast<std::uint16_t>(std::max(1U, end - start));
byte(out, 0x21); byte(out, 0xF9); byte(out, 0x04);
byte(out, 0x09); // restore background before next frame, index 0 transparent
le16(out, delay);
byte(out, 0); byte(out, 0);
byte(out, 0x2C); // full-canvas image descriptor
le16(out, 0); le16(out, 0);
le16(out, kWidth); le16(out, kHeight);
byte(out, 0); // use global palette
byte(out, 8); // LZW minimum code size
const std::vector<std::uint8_t> pixels =
gifPixels(document.frames[i], document.palette);
const std::vector<std::uint8_t> compressed = gifLzw(pixels);
for (std::size_t offset = 0; offset < compressed.size();) {
const std::size_t length =
std::min<std::size_t>(255, compressed.size() - offset);
byte(out, static_cast<std::uint8_t>(length));
out.write(reinterpret_cast<const char*>(compressed.data() + offset),
static_cast<std::streamsize>(length));
offset += length;
}
byte(out, 0); // terminate LZW sub-blocks
if (!out) {
error = "could not write GIF frame";
return false;
}
}
byte(out, 0x3B);
out.flush();
if (!out) {
error = "could not finish GIF output";
return false;
}
error.clear();
return true;
}
bool exportPngCover(const Document& document, const std::string& path,
std::string& error) {
if (!checkDocument(document, true, error)) return false;
if (path.empty()) {
error = "PNG output path is empty";
return false;
}
const Frame& frame = document.frames[document.metadata.coverFrame];
std::vector<std::uint8_t> raw;
raw.reserve(static_cast<std::size_t>(kHeight) * (1U + 4U * kWidth));
for (std::size_t y = 0; y < kHeight; ++y) {
raw.push_back(0); // PNG filter type 0
for (std::size_t x = 0; x < kWidth; ++x) {
const Color pixel = compositePixel(frame, document.palette,
y * kWidth + x);
raw.push_back(pixel.r);
raw.push_back(pixel.g);
raw.push_back(pixel.b);
raw.push_back(pixel.a);
}
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) {
error = "could not open PNG output";
return false;
}
constexpr std::array<std::uint8_t, 8> signature =
{137, 80, 78, 71, 13, 10, 26, 10};
out.write(reinterpret_cast<const char*>(signature.data()),
static_cast<std::streamsize>(signature.size()));
const std::vector<std::uint8_t> header = {
static_cast<std::uint8_t>(kWidth >> 24U),
static_cast<std::uint8_t>(kWidth >> 16U),
static_cast<std::uint8_t>(kWidth >> 8U),
static_cast<std::uint8_t>(kWidth),
static_cast<std::uint8_t>(kHeight >> 24U),
static_cast<std::uint8_t>(kHeight >> 16U),
static_cast<std::uint8_t>(kHeight >> 8U),
static_cast<std::uint8_t>(kHeight),
8, 6, 0, 0, 0}; // RGBA, 8 bits/channel, no interlace
pngChunk(out, "IHDR", header);
pngChunk(out, "IDAT", zlibStored(raw));
pngChunk(out, "IEND", {});
out.flush();
if (!out) {
error = "could not finish PNG output";
return false;
}
error.clear();
return true;
}
bool exportMp4(const Document& document, const std::string& path,
std::string& error) {
if (!checkDocument(document, false, error)) return false;
if (path.empty()) { error = "MP4 output path is empty"; return false; }
const double fps = frameRate(document.metadata);
if (!(fps > 0.0 && fps <= 30.0)) { error = "frame rate must be between 0 and 30 fps"; return false; }
Bytes ftyp;
auto ftypBox = boxBegin(ftyp, "ftyp");
type(ftyp, "isom"); be32(ftyp, 0x00000200); type(ftyp, "isom"); type(ftyp, "iso2"); type(ftyp, "mp41"); type(ftyp, "mjpg");
boxEnd(ftyp, ftypBox);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) { error = "could not create MP4 file"; return false; }
out.write(reinterpret_cast<const char*>(ftyp.data()), static_cast<std::streamsize>(ftyp.size()));
const std::uint32_t mdatStart = static_cast<std::uint32_t>(ftyp.size());
const std::uint8_t mdatHeader[8] = {0,0,0,0,'m','d','a','t'};
out.write(reinterpret_cast<const char*>(mdatHeader), sizeof(mdatHeader));
std::vector<std::uint32_t> sampleSizes;
std::vector<std::uint32_t> sampleOffsets;
sampleSizes.reserve(document.frames.size());
sampleOffsets.reserve(document.frames.size());
constexpr int jpegCapacity = 2 * 1024 * 1024;
std::vector<std::uint8_t> jpegBuffer(static_cast<std::size_t>(jpegCapacity));
for (std::size_t frameIndex = 0; frameIndex < document.frames.size(); ++frameIndex) {
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, kWidth, kHeight, 32, SDL_PIXELFORMAT_RGBA32);
if (!surface) { error = "could not allocate frame image"; return false; }
if (SDL_LockSurface(surface) != 0) {
SDL_FreeSurface(surface); error = "could not prepare frame image"; return false;
}
auto* rows = static_cast<std::uint8_t*>(surface->pixels);
for (std::size_t y = 0; y < kHeight; ++y) {
auto* row = reinterpret_cast<std::uint32_t*>(rows + y * static_cast<std::size_t>(surface->pitch));
for (std::size_t x = 0; x < kWidth; ++x) {
const Color pixel = opaqueFramePixel(document.frames[frameIndex], document.palette, y * kWidth + x);
row[x] = SDL_MapRGBA(surface->format, pixel.r, pixel.g, pixel.b, 255);
}
}
SDL_UnlockSurface(surface);
SDL_RWops* jpegStream = SDL_RWFromMem(jpegBuffer.data(), jpegCapacity);
if (!jpegStream || IMG_SaveJPG_RW(surface, jpegStream, 0, 82) != 0) {
if (jpegStream) SDL_RWclose(jpegStream);
SDL_FreeSurface(surface); error = "could not encode an MP4 frame as JPEG"; return false;
}
const Sint64 jpegSize = SDL_RWtell(jpegStream);
SDL_RWclose(jpegStream);
SDL_FreeSurface(surface);
if (jpegSize <= 0 || jpegSize > jpegCapacity) { error = "encoded frame is too large"; return false; }
const auto position = out.tellp();
if (position < 0 || static_cast<std::uint64_t>(position) > UINT32_MAX) {
error = "MP4 exceeds the supported 4 GB file limit"; return false;
}
sampleOffsets.push_back(static_cast<std::uint32_t>(position));
sampleSizes.push_back(static_cast<std::uint32_t>(jpegSize));
out.write(reinterpret_cast<const char*>(jpegBuffer.data()), jpegSize);
if (!out) { error = "could not write MP4 frame data"; return false; }
}
const auto endOfMedia = out.tellp();
if (endOfMedia < 0 || static_cast<std::uint64_t>(endOfMedia) - mdatStart > UINT32_MAX) {
error = "MP4 exceeds the supported 4 GB file limit"; return false;
}
const std::uint32_t mdatSize = static_cast<std::uint32_t>(static_cast<std::uint64_t>(endOfMedia) - mdatStart);
out.seekp(mdatStart);
const std::uint8_t encodedMdatSize[4] = {
static_cast<std::uint8_t>(mdatSize >> 24), static_cast<std::uint8_t>(mdatSize >> 16),
static_cast<std::uint8_t>(mdatSize >> 8), static_cast<std::uint8_t>(mdatSize)};
out.write(reinterpret_cast<const char*>(encodedMdatSize), sizeof(encodedMdatSize));
out.seekp(endOfMedia);
const Bytes moov = mp4MovieBox(sampleSizes, sampleOffsets, fps);
out.write(reinterpret_cast<const char*>(moov.data()), static_cast<std::streamsize>(moov.size()));
out.flush();
if (!out) { error = "could not finish MP4 index"; return false; }
out.close();
error.clear();
return true;
}
} // namespace sppm