feat: video player
This commit is contained in:
@@ -163,6 +163,10 @@ static bool is_audio_file(const char* name) {
|
||||
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
|
||||
}
|
||||
|
||||
static bool is_video_file(const char* name) {
|
||||
return str_ends_with(name, ".avi");
|
||||
}
|
||||
|
||||
static int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
@@ -1079,6 +1083,8 @@ static void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
montauk::spawn("0:/apps/wordprocessor/wordprocessor.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (str_ends_with(fm->entry_names[idx], ".elf")) {
|
||||
montauk::spawn(fullpath);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Makefile for video (standalone Videos app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-I . \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp avi.cpp stb_image_impl.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/video/video.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/video
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
HEADERS := avi.hpp
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp $(HEADERS) Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,610 @@
|
||||
#include "avi.hpp"
|
||||
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <gui/stb_image.h>
|
||||
}
|
||||
|
||||
namespace avi {
|
||||
|
||||
static constexpr uint32_t fourcc(char a, char b, char c, char d) {
|
||||
return (uint32_t)(uint8_t)a |
|
||||
((uint32_t)(uint8_t)b << 8) |
|
||||
((uint32_t)(uint8_t)c << 16) |
|
||||
((uint32_t)(uint8_t)d << 24);
|
||||
}
|
||||
|
||||
static constexpr uint32_t FCC_RIFF = fourcc('R', 'I', 'F', 'F');
|
||||
static constexpr uint32_t FCC_LIST = fourcc('L', 'I', 'S', 'T');
|
||||
static constexpr uint32_t FCC_AVI = fourcc('A', 'V', 'I', ' ');
|
||||
static constexpr uint32_t FCC_hdrl = fourcc('h', 'd', 'r', 'l');
|
||||
static constexpr uint32_t FCC_strl = fourcc('s', 't', 'r', 'l');
|
||||
static constexpr uint32_t FCC_movi = fourcc('m', 'o', 'v', 'i');
|
||||
static constexpr uint32_t FCC_rec = fourcc('r', 'e', 'c', ' ');
|
||||
static constexpr uint32_t FCC_avih = fourcc('a', 'v', 'i', 'h');
|
||||
static constexpr uint32_t FCC_strh = fourcc('s', 't', 'r', 'h');
|
||||
static constexpr uint32_t FCC_strf = fourcc('s', 't', 'r', 'f');
|
||||
static constexpr uint32_t FCC_vids = fourcc('v', 'i', 'd', 's');
|
||||
static constexpr uint32_t FCC_auds = fourcc('a', 'u', 'd', 's');
|
||||
static constexpr uint32_t FCC_MJPG = fourcc('M', 'J', 'P', 'G');
|
||||
static constexpr uint32_t FCC_DIB = fourcc('D', 'I', 'B', ' ');
|
||||
|
||||
static uint16_t rd_u16(const uint8_t* p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
|
||||
static uint32_t rd_u32(const uint8_t* p) {
|
||||
return (uint32_t)p[0] |
|
||||
((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) |
|
||||
((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static int32_t rd_s32(const uint8_t* p) {
|
||||
return (int32_t)rd_u32(p);
|
||||
}
|
||||
|
||||
static uint32_t min_u32(uint32_t a, uint32_t b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
static uint64_t align2(uint64_t n) {
|
||||
return (n + 1u) & ~1u;
|
||||
}
|
||||
|
||||
static void set_error(File& file, const char* msg) {
|
||||
montauk::strncpy(file.error, msg, (int)sizeof(file.error));
|
||||
}
|
||||
|
||||
static bool read_exact(const File& file, uint64_t offset, uint8_t* out, uint32_t size) {
|
||||
if (size == 0) return true;
|
||||
if (file.fd < 0 || !out) return false;
|
||||
if (offset > file.size || (uint64_t)size > file.size - offset) return false;
|
||||
|
||||
uint32_t done = 0;
|
||||
while (done < size) {
|
||||
uint32_t chunk = size - done;
|
||||
if (chunk > 32768u) chunk = 32768u;
|
||||
int rd = montauk::read(file.fd, out + done, offset + (uint64_t)done, chunk);
|
||||
if (rd <= 0) return false;
|
||||
done += (uint32_t)rd;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_bytes(const File& file, uint64_t offset, uint8_t* out, uint32_t size) {
|
||||
return read_exact(file, offset, out, size);
|
||||
}
|
||||
|
||||
static bool push_chunk(ChunkRef*& arr, int& count, int& cap,
|
||||
uint32_t offset, uint32_t size) {
|
||||
if (count >= cap) {
|
||||
int new_cap = cap ? cap * 2 : 256;
|
||||
auto* next = (ChunkRef*)montauk::realloc(arr, (uint64_t)new_cap * sizeof(ChunkRef));
|
||||
if (!next) return false;
|
||||
arr = next;
|
||||
cap = new_cap;
|
||||
}
|
||||
arr[count].offset = offset;
|
||||
arr[count].size = size;
|
||||
count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct StreamDesc {
|
||||
uint32_t type = 0;
|
||||
uint32_t handler = 0;
|
||||
uint32_t scale = 0;
|
||||
uint32_t rate = 0;
|
||||
uint32_t length = 0;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool top_down = false;
|
||||
int bits_per_pixel = 0;
|
||||
VideoCodec codec = VideoCodec::None;
|
||||
|
||||
uint16_t audio_format = 0;
|
||||
uint16_t audio_channels = 0;
|
||||
uint16_t audio_bits = 0;
|
||||
uint16_t audio_block_align = 0;
|
||||
uint32_t audio_rate = 0;
|
||||
uint32_t audio_avg_bytes_per_sec = 0;
|
||||
};
|
||||
|
||||
static bool chunk_stream_id(uint32_t id, int* stream_index, char* kind0, char* kind1) {
|
||||
char a = (char)(id & 0xFF);
|
||||
char b = (char)((id >> 8) & 0xFF);
|
||||
char c = (char)((id >> 16) & 0xFF);
|
||||
char d = (char)((id >> 24) & 0xFF);
|
||||
|
||||
if (a < '0' || a > '9' || b < '0' || b > '9')
|
||||
return false;
|
||||
|
||||
if (stream_index) *stream_index = (a - '0') * 10 + (b - '0');
|
||||
if (kind0) *kind0 = c;
|
||||
if (kind1) *kind1 = d;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_video_stream(File& file, int stream_index, const StreamDesc& desc,
|
||||
int& video_stream_index) {
|
||||
if (video_stream_index >= 0) return;
|
||||
if (desc.type != FCC_vids) return;
|
||||
if (desc.codec == VideoCodec::None) return;
|
||||
if (desc.width <= 0 || desc.height <= 0) return;
|
||||
|
||||
file.width = desc.width;
|
||||
file.height = desc.height;
|
||||
file.top_down = desc.top_down;
|
||||
file.bits_per_pixel = desc.bits_per_pixel;
|
||||
file.codec = desc.codec;
|
||||
file.stream_scale = desc.scale;
|
||||
file.stream_rate = desc.rate;
|
||||
file.declared_frame_count = desc.length;
|
||||
video_stream_index = stream_index;
|
||||
}
|
||||
|
||||
static void apply_audio_stream(File& file, int stream_index, const StreamDesc& desc,
|
||||
int& audio_stream_index) {
|
||||
if (audio_stream_index >= 0) return;
|
||||
if (desc.type != FCC_auds) return;
|
||||
if (desc.audio_format != 1) return;
|
||||
if (desc.audio_channels < 1 || desc.audio_channels > 2) return;
|
||||
if (desc.audio_bits != 8 && desc.audio_bits != 16) return;
|
||||
if (desc.audio_block_align == 0 || desc.audio_rate == 0) return;
|
||||
|
||||
file.audio_format = desc.audio_format;
|
||||
file.audio_channels = desc.audio_channels;
|
||||
file.audio_bits = desc.audio_bits;
|
||||
file.audio_block_align = desc.audio_block_align;
|
||||
file.audio_rate = desc.audio_rate;
|
||||
file.audio_avg_bytes_per_sec = desc.audio_avg_bytes_per_sec;
|
||||
file.audio_supported = true;
|
||||
audio_stream_index = stream_index;
|
||||
}
|
||||
|
||||
static bool parse_strl(File& file, uint64_t start, uint64_t size,
|
||||
int& stream_counter, int& video_stream_index, int& audio_stream_index) {
|
||||
StreamDesc desc = {};
|
||||
uint64_t pos = start;
|
||||
uint64_t end = start + size;
|
||||
uint8_t buf[64];
|
||||
|
||||
while (pos + 8 <= end) {
|
||||
if (!read_exact(file, pos, buf, 8)) {
|
||||
set_error(file, "Failed to read AVI stream header");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t id = rd_u32(buf + 0);
|
||||
uint32_t chunk_size = rd_u32(buf + 4);
|
||||
uint64_t payload = pos + 8;
|
||||
uint64_t next = payload + align2(chunk_size);
|
||||
if (next > end || next > file.size) {
|
||||
set_error(file, "AVI stream chunk is truncated");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id == FCC_strh && chunk_size >= 48) {
|
||||
uint32_t bytes = min_u32(chunk_size, (uint32_t)sizeof(buf));
|
||||
if (!read_exact(file, payload, buf, bytes)) {
|
||||
set_error(file, "Failed to read AVI stream format");
|
||||
return false;
|
||||
}
|
||||
desc.type = rd_u32(buf + 0);
|
||||
desc.handler = rd_u32(buf + 4);
|
||||
desc.scale = rd_u32(buf + 20);
|
||||
desc.rate = rd_u32(buf + 24);
|
||||
desc.length = rd_u32(buf + 32);
|
||||
} else if (id == FCC_strf) {
|
||||
uint32_t bytes = min_u32(chunk_size, (uint32_t)sizeof(buf));
|
||||
if (!read_exact(file, payload, buf, bytes)) {
|
||||
set_error(file, "Failed to read AVI stream data");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (desc.type == FCC_vids && chunk_size >= 40) {
|
||||
int32_t width = rd_s32(buf + 4);
|
||||
int32_t height = rd_s32(buf + 8);
|
||||
uint16_t bit_count = rd_u16(buf + 14);
|
||||
uint32_t compression = rd_u32(buf + 16);
|
||||
|
||||
desc.width = width > 0 ? width : -width;
|
||||
desc.height = height > 0 ? height : -height;
|
||||
desc.top_down = height < 0;
|
||||
desc.bits_per_pixel = bit_count;
|
||||
|
||||
if ((compression == 0 || compression == FCC_DIB) && bit_count == 24)
|
||||
desc.codec = VideoCodec::RGB24;
|
||||
else if ((compression == 0 || compression == FCC_DIB) && bit_count == 32)
|
||||
desc.codec = VideoCodec::RGB32;
|
||||
else if (compression == FCC_MJPG)
|
||||
desc.codec = VideoCodec::MJPG;
|
||||
} else if (desc.type == FCC_auds && chunk_size >= 16) {
|
||||
desc.audio_format = rd_u16(buf + 0);
|
||||
desc.audio_channels = rd_u16(buf + 2);
|
||||
desc.audio_rate = rd_u32(buf + 4);
|
||||
desc.audio_avg_bytes_per_sec = rd_u32(buf + 8);
|
||||
desc.audio_block_align = rd_u16(buf + 12);
|
||||
desc.audio_bits = rd_u16(buf + 14);
|
||||
}
|
||||
}
|
||||
|
||||
pos = next;
|
||||
}
|
||||
|
||||
apply_video_stream(file, stream_counter, desc, video_stream_index);
|
||||
apply_audio_stream(file, stream_counter, desc, audio_stream_index);
|
||||
stream_counter++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_hdrl(File& file, uint64_t start, uint64_t size,
|
||||
int& stream_counter, int& video_stream_index, int& audio_stream_index) {
|
||||
uint64_t pos = start;
|
||||
uint64_t end = start + size;
|
||||
uint8_t buf[48];
|
||||
|
||||
while (pos + 8 <= end) {
|
||||
if (!read_exact(file, pos, buf, 8)) {
|
||||
set_error(file, "Failed to read AVI header");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t id = rd_u32(buf + 0);
|
||||
uint32_t chunk_size = rd_u32(buf + 4);
|
||||
uint64_t payload = pos + 8;
|
||||
uint64_t next = payload + align2(chunk_size);
|
||||
if (next > end || next > file.size) {
|
||||
set_error(file, "AVI header chunk is truncated");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id == FCC_avih && chunk_size >= 40) {
|
||||
if (!read_exact(file, payload, buf, 40)) {
|
||||
set_error(file, "Failed to read AVI main header");
|
||||
return false;
|
||||
}
|
||||
file.microsec_per_frame = rd_u32(buf + 0);
|
||||
if (file.declared_frame_count == 0)
|
||||
file.declared_frame_count = rd_u32(buf + 16);
|
||||
} else if (id == FCC_LIST && chunk_size >= 4) {
|
||||
if (!read_exact(file, payload, buf, 4)) {
|
||||
set_error(file, "Failed to read AVI list header");
|
||||
return false;
|
||||
}
|
||||
uint32_t list_type = rd_u32(buf + 0);
|
||||
if (list_type == FCC_strl) {
|
||||
if (!parse_strl(file, payload + 4, chunk_size - 4,
|
||||
stream_counter, video_stream_index, audio_stream_index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos = next;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_movi_list(File& file, uint64_t start, uint64_t size,
|
||||
int video_stream_index, int audio_stream_index) {
|
||||
uint64_t pos = start;
|
||||
uint64_t end = start + size;
|
||||
uint8_t buf[12];
|
||||
|
||||
while (pos + 8 <= end) {
|
||||
if (!read_exact(file, pos, buf, 8)) {
|
||||
set_error(file, "Failed to read AVI media index");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t id = rd_u32(buf + 0);
|
||||
uint32_t chunk_size = rd_u32(buf + 4);
|
||||
uint64_t payload = pos + 8;
|
||||
uint64_t next = payload + align2(chunk_size);
|
||||
if (next > end || next > file.size) {
|
||||
set_error(file, "AVI media chunk is truncated");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id == FCC_LIST && chunk_size >= 4) {
|
||||
if (!read_exact(file, payload, buf, 4)) {
|
||||
set_error(file, "Failed to read AVI media list header");
|
||||
return false;
|
||||
}
|
||||
uint32_t list_type = rd_u32(buf + 0);
|
||||
if (list_type == FCC_rec || list_type == FCC_movi) {
|
||||
if (!parse_movi_list(file, payload + 4, chunk_size - 4,
|
||||
video_stream_index, audio_stream_index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
pos = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
int stream_index = -1;
|
||||
char kind0 = 0;
|
||||
char kind1 = 0;
|
||||
if (!chunk_stream_id(id, &stream_index, &kind0, &kind1)) {
|
||||
pos = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stream_index == video_stream_index &&
|
||||
((kind0 == 'd' && kind1 == 'b') || (kind0 == 'd' && kind1 == 'c') ||
|
||||
(kind0 == 'D' && kind1 == 'B') || (kind0 == 'D' && kind1 == 'C'))) {
|
||||
if (!push_chunk(file.video_chunks, file.video_chunk_count, file.video_chunk_cap,
|
||||
(uint32_t)payload, chunk_size)) {
|
||||
set_error(file, "Out of memory indexing video frames");
|
||||
return false;
|
||||
}
|
||||
} else if (stream_index == audio_stream_index && kind0 == 'w' && kind1 == 'b') {
|
||||
if (!push_chunk(file.audio_chunks, file.audio_chunk_count, file.audio_chunk_cap,
|
||||
(uint32_t)payload, chunk_size)) {
|
||||
set_error(file, "Out of memory indexing audio chunks");
|
||||
return false;
|
||||
}
|
||||
file.total_audio_bytes += chunk_size;
|
||||
}
|
||||
|
||||
pos = next;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset(File& file) {
|
||||
if (file.fd >= 0) montauk::close(file.fd);
|
||||
if (file.video_chunks) montauk::mfree(file.video_chunks);
|
||||
if (file.audio_chunks) montauk::mfree(file.audio_chunks);
|
||||
if (file.decode_buffer) montauk::mfree(file.decode_buffer);
|
||||
montauk::memset(&file, 0, sizeof(file));
|
||||
file.fd = -1;
|
||||
}
|
||||
|
||||
bool parse(File& file, int fd, uint64_t size) {
|
||||
reset(file);
|
||||
file.fd = fd;
|
||||
file.size = size;
|
||||
|
||||
uint8_t buf[12];
|
||||
if (size < 12) {
|
||||
set_error(file, "File is too small to be a valid AVI");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!read_exact(file, 0, buf, 12)) {
|
||||
set_error(file, "Failed to read AVI header");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rd_u32(buf + 0) != FCC_RIFF || rd_u32(buf + 8) != FCC_AVI) {
|
||||
set_error(file, "File is not a RIFF AVI container");
|
||||
return false;
|
||||
}
|
||||
|
||||
int stream_counter = 0;
|
||||
int video_stream_index = -1;
|
||||
int audio_stream_index = -1;
|
||||
|
||||
uint64_t pos = 12;
|
||||
while (pos + 8 <= size) {
|
||||
if (!read_exact(file, pos, buf, 8)) {
|
||||
set_error(file, "Failed to read AVI top-level chunk");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t id = rd_u32(buf + 0);
|
||||
uint32_t chunk_size = rd_u32(buf + 4);
|
||||
uint64_t payload = pos + 8;
|
||||
uint64_t next = payload + align2(chunk_size);
|
||||
if (next > size) {
|
||||
set_error(file, "AVI top-level chunk is truncated");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id == FCC_LIST && chunk_size >= 4) {
|
||||
if (!read_exact(file, payload, buf, 4)) {
|
||||
set_error(file, "Failed to read AVI list type");
|
||||
return false;
|
||||
}
|
||||
uint32_t list_type = rd_u32(buf + 0);
|
||||
if (list_type == FCC_hdrl) {
|
||||
if (!parse_hdrl(file, payload + 4, chunk_size - 4,
|
||||
stream_counter, video_stream_index, audio_stream_index)) {
|
||||
return false;
|
||||
}
|
||||
} else if (list_type == FCC_movi) {
|
||||
if (!parse_movi_list(file, payload + 4, chunk_size - 4,
|
||||
video_stream_index, audio_stream_index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos = next;
|
||||
}
|
||||
|
||||
if (video_stream_index < 0 || file.video_chunk_count <= 0 || file.width <= 0 || file.height <= 0) {
|
||||
set_error(file, "AVI video stream is missing or unsupported");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.audio_supported && file.audio_block_align > 0) {
|
||||
file.total_audio_frames = file.total_audio_bytes / file.audio_block_align;
|
||||
}
|
||||
|
||||
file.valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ensure_decode_buffer(File& file, uint32_t need) {
|
||||
if (need <= file.decode_buffer_cap) return true;
|
||||
|
||||
uint32_t new_cap = file.decode_buffer_cap ? file.decode_buffer_cap : 4096u;
|
||||
while (new_cap < need) {
|
||||
if (new_cap > 0x7FFFFFFFu / 2u) {
|
||||
new_cap = need;
|
||||
break;
|
||||
}
|
||||
new_cap *= 2u;
|
||||
}
|
||||
|
||||
auto* next = (uint8_t*)montauk::realloc(file.decode_buffer, new_cap);
|
||||
if (!next) return false;
|
||||
file.decode_buffer = next;
|
||||
file.decode_buffer_cap = new_cap;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_rgb(File& file, const ChunkRef& chunk, uint32_t* out_argb) {
|
||||
if (!out_argb || file.width <= 0 || file.height <= 0) return false;
|
||||
|
||||
int bytes_per_pixel = (file.codec == VideoCodec::RGB32) ? 4 : 3;
|
||||
int stride = ((file.width * bytes_per_pixel) + 3) & ~3;
|
||||
uint64_t needed64 = (uint64_t)stride * (uint64_t)file.height;
|
||||
if (chunk.size < needed64 || needed64 > 0xFFFFFFFFull) return false;
|
||||
|
||||
uint32_t needed = (uint32_t)needed64;
|
||||
if (!ensure_decode_buffer(file, needed)) return false;
|
||||
if (!read_exact(file, chunk.offset, file.decode_buffer, needed)) return false;
|
||||
|
||||
const uint8_t* src = file.decode_buffer;
|
||||
for (int y = 0; y < file.height; y++) {
|
||||
int src_y = file.top_down ? y : (file.height - 1 - y);
|
||||
const uint8_t* row = src + (uint64_t)src_y * (uint64_t)stride;
|
||||
uint32_t* dst = out_argb + (uint64_t)y * (uint64_t)file.width;
|
||||
|
||||
for (int x = 0; x < file.width; x++) {
|
||||
uint8_t b = row[x * bytes_per_pixel + 0];
|
||||
uint8_t g = row[x * bytes_per_pixel + 1];
|
||||
uint8_t r = row[x * bytes_per_pixel + 2];
|
||||
dst[x] = 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void scale_rgba_to_argb(uint32_t* out_argb, int out_w, int out_h,
|
||||
const uint8_t* src_rgba, int src_w, int src_h) {
|
||||
for (int y = 0; y < out_h; y++) {
|
||||
int sy = (int)((uint64_t)y * (uint64_t)src_h / (uint64_t)out_h);
|
||||
const uint8_t* src_row = src_rgba + (uint64_t)sy * (uint64_t)src_w * 4u;
|
||||
uint32_t* dst = out_argb + (uint64_t)y * (uint64_t)out_w;
|
||||
|
||||
for (int x = 0; x < out_w; x++) {
|
||||
int sx = (int)((uint64_t)x * (uint64_t)src_w / (uint64_t)out_w);
|
||||
const uint8_t* p = src_row + (uint64_t)sx * 4u;
|
||||
dst[x] = 0xFF000000u | ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | p[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_mjpg(File& file, const ChunkRef& chunk, uint32_t* out_argb) {
|
||||
if (chunk.size > 0x7FFFFFFFu) return false;
|
||||
if (!ensure_decode_buffer(file, chunk.size)) return false;
|
||||
if (!read_exact(file, chunk.offset, file.decode_buffer, chunk.size)) return false;
|
||||
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int channels = 0;
|
||||
unsigned char* rgba = stbi_load_from_memory(file.decode_buffer, (int)chunk.size,
|
||||
&w, &h, &channels, 4);
|
||||
if (!rgba || w <= 0 || h <= 0) {
|
||||
if (rgba) stbi_image_free(rgba);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (w == file.width && h == file.height) {
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
uint8_t r = rgba[i * 4 + 0];
|
||||
uint8_t g = rgba[i * 4 + 1];
|
||||
uint8_t b = rgba[i * 4 + 2];
|
||||
out_argb[i] = 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
} else {
|
||||
scale_rgba_to_argb(out_argb, file.width, file.height, rgba, w, h);
|
||||
}
|
||||
|
||||
stbi_image_free(rgba);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_frame(File& file, int frame_index, uint32_t* out_argb) {
|
||||
if (!file.valid || !out_argb) return false;
|
||||
if (frame_index < 0 || frame_index >= file.video_chunk_count) return false;
|
||||
|
||||
const ChunkRef& chunk = file.video_chunks[frame_index];
|
||||
if (file.codec == VideoCodec::RGB24 || file.codec == VideoCodec::RGB32)
|
||||
return decode_rgb(file, chunk, out_argb);
|
||||
if (file.codec == VideoCodec::MJPG)
|
||||
return decode_mjpg(file, chunk, out_argb);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t video_duration_us(const File& file) {
|
||||
if (file.video_chunk_count <= 0) return 0;
|
||||
|
||||
if (file.stream_rate > 0 && file.stream_scale > 0) {
|
||||
return ((uint64_t)file.video_chunk_count * (uint64_t)file.stream_scale * 1000000ull) /
|
||||
(uint64_t)file.stream_rate;
|
||||
}
|
||||
if (file.microsec_per_frame > 0)
|
||||
return (uint64_t)file.video_chunk_count * (uint64_t)file.microsec_per_frame;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t audio_duration_us(const File& file) {
|
||||
if (!file.audio_supported || file.audio_rate == 0 || file.total_audio_frames == 0) return 0;
|
||||
return (file.total_audio_frames * 1000000ull) / (uint64_t)file.audio_rate;
|
||||
}
|
||||
|
||||
uint64_t duration_us(const File& file) {
|
||||
uint64_t v = video_duration_us(file);
|
||||
uint64_t a = audio_duration_us(file);
|
||||
return v > a ? v : a;
|
||||
}
|
||||
|
||||
int frame_index_for_time(const File& file, uint64_t time_us) {
|
||||
if (file.video_chunk_count <= 0) return -1;
|
||||
if (file.video_chunk_count == 1) return 0;
|
||||
|
||||
int index = 0;
|
||||
if (file.stream_rate > 0 && file.stream_scale > 0) {
|
||||
uint64_t denom = (uint64_t)file.stream_scale * 1000000ull;
|
||||
if (denom > 0)
|
||||
index = (int)((time_us * (uint64_t)file.stream_rate) / denom);
|
||||
} else if (file.microsec_per_frame > 0) {
|
||||
index = (int)(time_us / (uint64_t)file.microsec_per_frame);
|
||||
}
|
||||
|
||||
if (index < 0) index = 0;
|
||||
if (index >= file.video_chunk_count) index = file.video_chunk_count - 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* codec_name(const File& file) {
|
||||
switch (file.codec) {
|
||||
case VideoCodec::RGB24: return "RGB24";
|
||||
case VideoCodec::RGB32: return "RGB32";
|
||||
case VideoCodec::MJPG: return "MJPEG";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool has_supported_audio(const File& file) {
|
||||
return file.audio_supported && file.audio_chunk_count > 0;
|
||||
}
|
||||
|
||||
} // namespace avi
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace avi {
|
||||
|
||||
enum class VideoCodec {
|
||||
None,
|
||||
RGB24,
|
||||
RGB32,
|
||||
MJPG,
|
||||
};
|
||||
|
||||
struct ChunkRef {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
struct File {
|
||||
int fd;
|
||||
uint64_t size;
|
||||
|
||||
int width;
|
||||
int height;
|
||||
bool top_down;
|
||||
int bits_per_pixel;
|
||||
VideoCodec codec;
|
||||
|
||||
uint32_t stream_scale;
|
||||
uint32_t stream_rate;
|
||||
uint32_t microsec_per_frame;
|
||||
uint32_t declared_frame_count;
|
||||
|
||||
uint16_t audio_format;
|
||||
uint16_t audio_channels;
|
||||
uint16_t audio_bits;
|
||||
uint16_t audio_block_align;
|
||||
uint32_t audio_rate;
|
||||
uint32_t audio_avg_bytes_per_sec;
|
||||
bool audio_supported;
|
||||
|
||||
ChunkRef* video_chunks;
|
||||
int video_chunk_count;
|
||||
int video_chunk_cap;
|
||||
|
||||
ChunkRef* audio_chunks;
|
||||
int audio_chunk_count;
|
||||
int audio_chunk_cap;
|
||||
|
||||
uint64_t total_audio_bytes;
|
||||
uint64_t total_audio_frames;
|
||||
|
||||
uint8_t* decode_buffer;
|
||||
uint32_t decode_buffer_cap;
|
||||
|
||||
bool valid;
|
||||
char error[128];
|
||||
};
|
||||
|
||||
void reset(File& file);
|
||||
bool parse(File& file, int fd, uint64_t size);
|
||||
bool read_bytes(const File& file, uint64_t offset, uint8_t* out, uint32_t size);
|
||||
bool decode_frame(File& file, int frame_index, uint32_t* out_argb);
|
||||
|
||||
uint64_t video_duration_us(const File& file);
|
||||
uint64_t audio_duration_us(const File& file);
|
||||
uint64_t duration_us(const File& file);
|
||||
|
||||
int frame_index_for_time(const File& file, uint64_t time_us);
|
||||
const char* codec_name(const File& file);
|
||||
bool has_supported_audio(const File& file);
|
||||
|
||||
} // namespace avi
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Videos"
|
||||
binary = "video.elf"
|
||||
icon = "multimedia-video-player.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
visible = true
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* stb_image_impl.cpp
|
||||
* Single compilation unit for JPEG decoding in the MontaukOS Videos app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
extern "C" {
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
}
|
||||
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_NO_PNG
|
||||
#define STBI_NO_BMP
|
||||
#define STBI_NO_PSD
|
||||
#define STBI_NO_TGA
|
||||
#define STBI_NO_GIF
|
||||
#define STBI_NO_HDR
|
||||
#define STBI_NO_PIC
|
||||
#define STBI_NO_PNM
|
||||
#define STBI_NO_LINEAR
|
||||
#define STBI_NO_STDIO
|
||||
#define STBI_NO_THREAD_LOCALS
|
||||
|
||||
#define STBI_MALLOC(sz) malloc(sz)
|
||||
#define STBI_FREE(p) free(p)
|
||||
#define STBI_REALLOC(p, newsz) realloc(p, newsz)
|
||||
#define STBI_ASSERT(x) ((void)(x))
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <gui/stb_image.h>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user