/* * main.cpp * MontaukOS Videos app -- AVI playback with PCM audio * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include extern "C" { #include #include } #include "avi.hpp" using namespace gui; static constexpr int INIT_W = 1040; static constexpr int INIT_H = 720; static constexpr int PROGRESS_H = 40; static constexpr int TRANSPORT_H = 72; static constexpr int TOOL_ICON_SIZE = 16; static constexpr int FONT_SIZE = 18; static constexpr int FONT_SIZE_SM = 16; static constexpr int FONT_SIZE_LG = 22; static constexpr int MAX_FILES = 128; static constexpr int MAX_PATH = 256; static constexpr int MAX_FILTER = 64; static constexpr uint64_t MAX_FILE_SIZE = 1024ull * 1024ull * 1024ull; static constexpr int AUDIO_OUT_CHANNELS = 2; static constexpr int AUDIO_OUT_BITS = 16; static constexpr int AUDIO_RING_BYTES = 0x8000; static constexpr int AUDIO_CHUNK_BYTES = 4096; static constexpr Color BG_TOP = Color::from_rgb(0x07, 0x09, 0x0C); static constexpr Color BG_BOTTOM = Color::from_rgb(0x12, 0x15, 0x1A); static constexpr Color TOOLBAR_BG = Color::from_rgb(0x10, 0x13, 0x18); static constexpr Color PANEL_BG = Color::from_rgb(0x14, 0x18, 0x1F); static constexpr Color PANEL_BG_ALT = Color::from_rgb(0x0E, 0x12, 0x17); static constexpr Color VIDEO_BG = Color::from_rgb(0x03, 0x04, 0x06); static constexpr Color BORDER = Color::from_rgb(0x2A, 0x31, 0x3A); static constexpr Color TEXT = Color::from_rgb(0xF4, 0xF0, 0xE8); static constexpr Color TEXT_DIM = Color::from_rgb(0xA2, 0xA8, 0xB3); static constexpr Color TEXT_MUTED = Color::from_rgb(0x73, 0x7A, 0x84); static constexpr Color ACCENT = Color::from_rgb(0xD9, 0x4F, 0x32); static constexpr Color ACCENT_DARK = Color::from_rgb(0xA9, 0x35, 0x20); static constexpr Color ACCENT_SOFT = Color::from_rgb(0x3A, 0x1C, 0x18); static constexpr Color TRACK_BG = Color::from_rgb(0x29, 0x31, 0x3B); static constexpr Color BTN_BG = Color::from_rgb(0x1C, 0x23, 0x2C); static constexpr Color BTN_ACTIVE = Color::from_rgb(0xD9, 0x4F, 0x32); static constexpr Color BTN_STOP = Color::from_rgb(0xC9, 0x3A, 0x31); static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); enum class PlayState { Stopped, Playing, Paused, }; struct VideoEntry { char name[128]; }; struct AppState { int win_w; int win_h; TrueTypeFont* font; char dir_path[MAX_PATH]; VideoEntry files[MAX_FILES]; int file_count; int scroll_y; int hovered_item; int selected_item; int filtered_indices[MAX_FILES]; int filtered_count; char filter_text[MAX_FILTER]; bool filter_active; bool loop_enabled; int current_video; avi::File movie; uint32_t* frame_pixels; int displayed_frame_index; char status_text[128]; PlayState play_state; uint64_t base_time_us; uint64_t play_start_ms; int audio_handle; uint64_t audio_hw_total_bytes; uint64_t audio_output_written_bytes; uint64_t audio_output_total_bytes; int audio_hw_prev_pos; bool audio_hw_valid; int audio_chunk_index; uint32_t audio_chunk_pos; uint8_t audio_buf[AUDIO_CHUNK_BYTES]; int audio_buf_len; int audio_buf_pos; bool dragging_progress; uint64_t drag_time_us; SvgIcon ico_rewind; SvgIcon ico_play; SvgIcon ico_pause; SvgIcon ico_stop; SvgIcon ico_forward; SvgIcon ico_play_w; SvgIcon ico_pause_w; SvgIcon ico_stop_w; SvgIcon ico_repeat; SvgIcon ico_repeat_w; }; static AppState g = {}; static void transport_layout(int* out_x, int* out_y, int* out_w, int* out_h, int* out_gap); static int text_w(const char* text, int size = FONT_SIZE) { return (g.font && text) ? g.font->measure_text(text, size) : 0; } static int font_h(int size = FONT_SIZE) { if (!g.font) return size; GlyphCache* cache = g.font->get_cache(size); return cache ? (cache->ascent - cache->descent) : size; } static void px_fill(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c) { if (!px || w <= 0 || h <= 0) return; int x0 = gui_max(0, x); int y0 = gui_max(0, y); int x1 = gui_min(bw, x + w); int y1 = gui_min(bh, y + h); if (x0 >= x1 || y0 >= y1) return; uint32_t v = c.to_pixel(); for (int row = y0; row < y1; row++) { for (int col = x0; col < x1; col++) { px[row * bw + col] = v; } } } static void px_vgradient(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color top, Color bottom) { if (!px || w <= 0 || h <= 0) return; int x0 = gui_max(0, x); int y0 = gui_max(0, y); int x1 = gui_min(bw, x + w); int y1 = gui_min(bh, y + h); if (x0 >= x1 || y0 >= y1) return; for (int row = y0; row < y1; row++) { int t = (h <= 1) ? 255 : ((row - y) * 255) / (h - 1); int inv = 255 - t; Color c = { (uint8_t)((top.r * inv + bottom.r * t + 127) / 255), (uint8_t)((top.g * inv + bottom.g * t + 127) / 255), (uint8_t)((top.b * inv + bottom.b * t + 127) / 255), 255 }; uint32_t v = c.to_pixel(); for (int col = x0; col < x1; col++) { px[row * bw + col] = v; } } } static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) { if (!px || y < 0 || y >= bh || w <= 0) return; int x0 = gui_max(0, x); int x1 = gui_min(bw, x + w); uint32_t v = c.to_pixel(); for (int col = x0; col < x1; col++) { px[y * bw + col] = v; } } static void px_vline(uint32_t* px, int bw, int bh, int x, int y, int h, Color c) { if (!px || x < 0 || x >= bw || h <= 0) return; int y0 = gui_max(0, y); int y1 = gui_min(bh, y + h); uint32_t v = c.to_pixel(); for (int row = y0; row < y1; row++) { px[row * bw + x] = v; } } static void px_rect(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c) { if (w <= 0 || h <= 0) return; px_hline(px, bw, bh, x, y, w, c); px_hline(px, bw, bh, x, y + h - 1, w, c); px_vline(px, bw, bh, x, y, h, c); px_vline(px, bw, bh, x + w - 1, y, h, c); } static void px_fill_rounded(uint32_t* px, int bw, int bh, int x, int y, int w, int h, int r, Color c) { if (!px || w <= 0 || h <= 0) return; if (r < 0) r = 0; int max_r = gui_min(w / 2, h / 2); if (r > max_r) r = max_r; uint32_t v = c.to_pixel(); for (int row = 0; row < h; row++) { int py = y + row; if (py < 0 || py >= bh) continue; for (int col = 0; col < w; col++) { int px_x = x + col; if (px_x < 0 || px_x >= bw) continue; bool skip = false; int cx = 0, cy = 0; if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; skip = (cx * cx + cy * cy >= r * r); } else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; skip = (cx * cx + cy * cy >= r * r); } else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); skip = (cx * cx + cy * cy >= r * r); } else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); skip = (cx * cx + cy * cy >= r * r); } if (!skip) px[py * bw + px_x] = v; } } } static void px_circle(uint32_t* px, int bw, int bh, int cx, int cy, int r, Color c) { if (!px || r <= 0) return; uint32_t v = c.to_pixel(); for (int dy = -r; dy <= r; dy++) { int py = cy + dy; if (py < 0 || py >= bh) continue; for (int dx = -r; dx <= r; dx++) { int px_x = cx + dx; if (px_x < 0 || px_x >= bw) continue; if (dx * dx + dy * dy <= r * r) px[py * bw + px_x] = v; } } } static void px_icon(uint32_t* px, int bw, int bh, int x, int y, const SvgIcon& icon) { if (!px || !icon.pixels) return; for (int row = 0; row < icon.height; row++) { int dy = y + row; if (dy < 0 || dy >= bh) continue; for (int col = 0; col < icon.width; col++) { int dx = x + col; if (dx < 0 || dx >= bw) continue; uint32_t src = icon.pixels[row * icon.width + col]; uint8_t sa = (src >> 24) & 0xFF; if (sa == 0) continue; if (sa == 255) { px[dy * bw + dx] = src; } else { uint32_t dst = px[dy * bw + dx]; uint32_t inv = 255 - sa; uint32_t r = (sa * ((src >> 16) & 0xFF) + inv * ((dst >> 16) & 0xFF) + 127) / 255; uint32_t g0 = (sa * ((src >> 8) & 0xFF) + inv * ((dst >> 8) & 0xFF) + 127) / 255; uint32_t b = (sa * (src & 0xFF) + inv * (dst & 0xFF) + 127) / 255; px[dy * bw + dx] = 0xFF000000u | (r << 16) | (g0 << 8) | b; } } } } static void px_text(uint32_t* px, int bw, int bh, int x, int y, const char* text, Color c, int size = FONT_SIZE) { if (g.font && text && text[0]) g.font->draw_to_buffer(px, bw, bh, x, y, text, c, size); } static void copy_text_fit(char* out, int out_cap, const char* text, int max_w, int size) { if (!out || out_cap <= 0) return; if (!text) { out[0] = '\0'; return; } montauk::strncpy(out, text, out_cap); if (!g.font || text_w(out, size) <= max_w) return; int len = montauk::slen(out); const char* ellipsis = "..."; while (len > 0) { len--; out[len] = '\0'; char trial[160]; montauk::strncpy(trial, out, (int)sizeof(trial)); int tlen = montauk::slen(trial); if (tlen < (int)sizeof(trial) - 4) { trial[tlen + 0] = '.'; trial[tlen + 1] = '.'; trial[tlen + 2] = '.'; trial[tlen + 3] = '\0'; } if (text_w(trial, size) <= max_w || len == 0) { montauk::strncpy(out, trial, out_cap); return; } } montauk::strncpy(out, ellipsis, out_cap); } static void px_text_fit(uint32_t* px, int bw, int bh, int x, int y, int max_w, const char* text, Color c, int size = FONT_SIZE) { char fitted[160]; copy_text_fit(fitted, (int)sizeof(fitted), text, max_w, size); px_text(px, bw, bh, x, y, fitted, c, size); } static void px_icon_button(uint32_t* px, int bw, int bh, int x, int y, int w, int h, const SvgIcon& icon, Color bg, int r) { px_fill_rounded(px, bw, bh, x, y, w, h, r, bg); px_icon(px, bw, bh, x + (w - icon.width) / 2, y + (h - icon.height) / 2, icon); } static char ascii_lower(char c) { return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; } static bool is_printable_ascii(char c) { return c >= 32 && c < 127; } static bool str_ends_with(const char* s, const char* suffix) { int sl = montauk::slen(s); int xl = montauk::slen(suffix); if (xl > sl) return false; for (int i = 0; i < xl; i++) { if (ascii_lower(s[sl - xl + i]) != ascii_lower(suffix[i])) return false; } return true; } static bool str_eq_case_insensitive(const char* a, const char* b) { int i = 0; while (a[i] || b[i]) { if (ascii_lower(a[i]) != ascii_lower(b[i])) return false; i++; } return true; } static bool str_contains_case_insensitive(const char* text, const char* needle) { if (!needle || !needle[0]) return true; if (!text || !text[0]) return false; int tlen = montauk::slen(text); int nlen = montauk::slen(needle); if (nlen > tlen) return false; for (int i = 0; i <= tlen - nlen; i++) { bool match = true; for (int j = 0; j < nlen; j++) { if (ascii_lower(text[i + j]) != ascii_lower(needle[j])) { match = false; break; } } if (match) return true; } return false; } static void set_status(const char* text) { montauk::strncpy(g.status_text, text ? text : "", (int)sizeof(g.status_text)); } static uint64_t media_duration_us() { return g.movie.valid ? avi::duration_us(g.movie) : 0; } static Rect content_rect() { int bottom = g.win_h - PROGRESS_H - TRANSPORT_H; if (bottom < 160) bottom = 160; return {0, 0, g.win_w, bottom}; } static Rect video_rect() { return content_rect(); } static Rect repeat_button_rect() { int bx, by, bw, bh, gap; transport_layout(&bx, &by, &bw, &bh, &gap); (void)bx; (void)gap; return {g.win_w - bw - 24, by, bw, bh}; } static Rect progress_rect() { int y = g.win_h - TRANSPORT_H - PROGRESS_H; return {16, y + 28, g.win_w - 32, 6}; } static void transport_layout(int* out_x, int* out_y, int* out_w, int* out_h, int* out_gap) { int btn_w = 48; int btn_h = 32; int gap = 10; int total_w = btn_w * 4 + gap * 3; int x = (g.win_w - total_w) / 2; int y = g.win_h - TRANSPORT_H + (TRANSPORT_H - btn_h) / 2; if (out_x) *out_x = x; if (out_y) *out_y = y; if (out_w) *out_w = btn_w; if (out_h) *out_h = btn_h; if (out_gap) *out_gap = gap; } static int visible_items() { return 0; } static int filtered_raw_at(int visible_index) { if (visible_index < 0 || visible_index >= g.filtered_count) return -1; return g.filtered_indices[visible_index]; } static bool filtered_contains(int raw_index) { if (raw_index < 0) return false; for (int i = 0; i < g.filtered_count; i++) { if (g.filtered_indices[i] == raw_index) return true; } return false; } static int selected_visible_index() { for (int i = 0; i < g.filtered_count; i++) { if (g.filtered_indices[i] == g.selected_item) return i; } return -1; } static void clamp_list_scroll() { int max_scroll = g.filtered_count; if (max_scroll < 0) max_scroll = 0; if (g.scroll_y < 0) g.scroll_y = 0; if (g.scroll_y > max_scroll) g.scroll_y = max_scroll; } static void ensure_selected_visible() { int sel = selected_visible_index(); if (sel < 0) { g.scroll_y = 0; return; } g.scroll_y = sel; } static void rebuild_filtered_view() { int prev_selected = g.selected_item; g.filtered_count = 0; for (int i = 0; i < g.file_count && g.filtered_count < MAX_FILES; i++) { if (str_contains_case_insensitive(g.files[i].name, g.filter_text)) g.filtered_indices[g.filtered_count++] = i; } if (!filtered_contains(g.hovered_item)) g.hovered_item = -1; if (g.filtered_count == 0) { g.selected_item = -1; g.scroll_y = 0; return; } if (filtered_contains(prev_selected)) g.selected_item = prev_selected; else if (filtered_contains(g.current_video)) g.selected_item = g.current_video; else g.selected_item = g.filtered_indices[0]; clamp_list_scroll(); ensure_selected_visible(); } static void append_filter_char(char c) { int len = montauk::slen(g.filter_text); if (len >= MAX_FILTER - 1) return; g.filter_text[len] = c; g.filter_text[len + 1] = '\0'; rebuild_filtered_view(); } static void pop_filter_char() { int len = montauk::slen(g.filter_text); if (len <= 0) return; g.filter_text[len - 1] = '\0'; rebuild_filtered_view(); } static void clear_filter() { if (!g.filter_text[0]) return; g.filter_text[0] = '\0'; rebuild_filtered_view(); } static void move_selection(int delta) { if (g.filtered_count <= 0) { g.selected_item = -1; g.scroll_y = 0; return; } int sel = selected_visible_index(); if (sel < 0) sel = (delta >= 0) ? 0 : (g.filtered_count - 1); else { sel += delta; if (sel < 0) sel = 0; if (sel >= g.filtered_count) sel = g.filtered_count - 1; } g.selected_item = g.filtered_indices[sel]; ensure_selected_visible(); } static void scan_directory() { g.file_count = 0; const char* names[256]; int count = montauk::readdir(g.dir_path, names, 256); if (count < 0) { rebuild_filtered_view(); return; } for (int i = 0; i < count && g.file_count < MAX_FILES; i++) { if (!str_ends_with(names[i], ".avi")) continue; montauk::strncpy(g.files[g.file_count].name, names[i], (int)sizeof(g.files[g.file_count].name)); g.file_count++; } rebuild_filtered_view(); } static void close_audio_session() { if (g.audio_handle >= 0) { montauk::audio_close(g.audio_handle); g.audio_handle = -1; } g.audio_hw_total_bytes = 0; g.audio_output_written_bytes = 0; g.audio_output_total_bytes = 0; g.audio_hw_prev_pos = 0; g.audio_hw_valid = false; g.audio_chunk_index = 0; g.audio_chunk_pos = 0; g.audio_buf_len = 0; g.audio_buf_pos = 0; } static void unload_movie() { close_audio_session(); avi::reset(g.movie); if (g.frame_pixels) { montauk::mfree(g.frame_pixels); g.frame_pixels = nullptr; } g.current_video = -1; g.displayed_frame_index = -1; g.base_time_us = 0; g.play_start_ms = 0; g.play_state = PlayState::Stopped; g.dragging_progress = false; } static void format_time(char* buf, int buf_size, uint64_t time_us) { uint64_t secs = time_us / 1000000ull; int h = (int)(secs / 3600ull); int m = (int)((secs / 60ull) % 60ull); int s = (int)(secs % 60ull); if (h > 0) snprintf(buf, buf_size, "%d:%02d:%02d", h, m, s); else snprintf(buf, buf_size, "%d:%02d", m, s); } static void format_fps(char* buf, int buf_size) { if (!g.movie.valid) { snprintf(buf, buf_size, "-- fps"); return; } if (g.movie.stream_rate > 0 && g.movie.stream_scale > 0) { uint64_t fps10 = ((uint64_t)g.movie.stream_rate * 10ull + (uint64_t)g.movie.stream_scale / 2ull) / (uint64_t)g.movie.stream_scale; if ((fps10 % 10ull) == 0) snprintf(buf, buf_size, "%llu fps", (unsigned long long)(fps10 / 10ull)); else snprintf(buf, buf_size, "%llu.%llu fps", (unsigned long long)(fps10 / 10ull), (unsigned long long)(fps10 % 10ull)); return; } if (g.movie.microsec_per_frame > 0) { uint64_t fps10 = (10000000ull + (uint64_t)g.movie.microsec_per_frame / 2ull) / (uint64_t)g.movie.microsec_per_frame; if ((fps10 % 10ull) == 0) snprintf(buf, buf_size, "%llu fps", (unsigned long long)(fps10 / 10ull)); else snprintf(buf, buf_size, "%llu.%llu fps", (unsigned long long)(fps10 / 10ull), (unsigned long long)(fps10 % 10ull)); return; } snprintf(buf, buf_size, "-- fps"); } static bool movie_has_supported_audio() { return avi::has_supported_audio(g.movie); } static void update_audio_hw_position() { if (g.audio_handle < 0) return; int hw = montauk::audio_get_pos(g.audio_handle); if (hw < 0) return; int pos = hw % AUDIO_RING_BYTES; if (!g.audio_hw_valid) { g.audio_hw_prev_pos = pos; g.audio_hw_valid = true; return; } int delta = pos - g.audio_hw_prev_pos; if (delta < 0) delta += AUDIO_RING_BYTES; g.audio_hw_total_bytes += (uint64_t)delta; if (g.audio_hw_total_bytes > g.audio_output_written_bytes) g.audio_hw_total_bytes = g.audio_output_written_bytes; g.audio_hw_prev_pos = pos; } static uint64_t current_time_us() { uint64_t total = media_duration_us(); if (!g.movie.valid) return 0; if (g.dragging_progress) return g.drag_time_us > total ? total : g.drag_time_us; if (g.play_state == PlayState::Playing) { if (g.audio_handle >= 0) { update_audio_hw_position(); uint64_t bytes_per_sec = (uint64_t)g.movie.audio_rate * 4ull; if (bytes_per_sec > 0) { uint64_t delta_us = (g.audio_hw_total_bytes * 1000000ull) / bytes_per_sec; uint64_t t = g.base_time_us + delta_us; return t > total ? total : t; } } else { uint64_t elapsed_us = (montauk::get_milliseconds() - g.play_start_ms) * 1000ull; uint64_t t = g.base_time_us + elapsed_us; return t > total ? total : t; } } return g.base_time_us > total ? total : g.base_time_us; } static bool decode_frame_index(int index) { if (!g.movie.valid || !g.frame_pixels) return false; if (index < 0 || index >= g.movie.video_chunk_count) return false; if (index == g.displayed_frame_index) return true; if (!avi::decode_frame(g.movie, index, g.frame_pixels)) { set_status("Frame decode failed"); return false; } g.displayed_frame_index = index; return true; } static void sync_frame_to_time(uint64_t time_us) { if (!g.movie.valid) return; int index = avi::frame_index_for_time(g.movie, time_us); if (index >= 0) decode_frame_index(index); } static void seek_audio_to_time(uint64_t time_us) { g.audio_chunk_index = 0; g.audio_chunk_pos = 0; g.audio_buf_len = 0; g.audio_buf_pos = 0; if (!movie_has_supported_audio()) { g.audio_output_total_bytes = 0; return; } uint64_t target_frames = (time_us * (uint64_t)g.movie.audio_rate) / 1000000ull; if (target_frames > g.movie.total_audio_frames) target_frames = g.movie.total_audio_frames; uint64_t remaining_frames = g.movie.total_audio_frames - target_frames; g.audio_output_total_bytes = remaining_frames * 4ull; uint64_t skip_frames = target_frames; for (int i = 0; i < g.movie.audio_chunk_count; i++) { uint64_t chunk_frames = g.movie.audio_chunks[i].size / g.movie.audio_block_align; if (skip_frames >= chunk_frames) { skip_frames -= chunk_frames; continue; } g.audio_chunk_index = i; g.audio_chunk_pos = (uint32_t)(skip_frames * (uint64_t)g.movie.audio_block_align); return; } g.audio_chunk_index = g.movie.audio_chunk_count; g.audio_chunk_pos = 0; } static void fill_audio_buffer() { g.audio_buf_pos = 0; g.audio_buf_len = 0; if (!movie_has_supported_audio()) return; uint8_t src_buf[AUDIO_CHUNK_BYTES]; while (g.audio_buf_len + 4 <= AUDIO_CHUNK_BYTES && g.audio_chunk_index < g.movie.audio_chunk_count) { const avi::ChunkRef& chunk = g.movie.audio_chunks[g.audio_chunk_index]; if (g.audio_chunk_pos >= chunk.size) { g.audio_chunk_index++; g.audio_chunk_pos = 0; continue; } if (chunk.size - g.audio_chunk_pos < g.movie.audio_block_align) { g.audio_chunk_index++; g.audio_chunk_pos = 0; continue; } uint32_t out_space = (uint32_t)(AUDIO_CHUNK_BYTES - g.audio_buf_len); uint32_t max_frames = out_space / 4u; uint32_t src_space = chunk.size - g.audio_chunk_pos; src_space -= src_space % g.movie.audio_block_align; uint32_t read_size = max_frames * (uint32_t)g.movie.audio_block_align; if (read_size > src_space) read_size = src_space; if (read_size > AUDIO_CHUNK_BYTES) { read_size = AUDIO_CHUNK_BYTES; read_size -= read_size % g.movie.audio_block_align; } if (read_size < g.movie.audio_block_align) return; if (!avi::read_bytes(g.movie, (uint64_t)chunk.offset + (uint64_t)g.audio_chunk_pos, src_buf, read_size)) { set_status("Audio read failed"); g.audio_chunk_index = g.movie.audio_chunk_count; g.audio_chunk_pos = 0; return; } uint32_t frames = read_size / g.movie.audio_block_align; for (uint32_t i = 0; i < frames; i++) { const uint8_t* src = src_buf + (uint64_t)i * (uint64_t)g.movie.audio_block_align; int16_t left = 0; int16_t right = 0; if (g.movie.audio_bits == 8) { int s0 = ((int)src[0] - 128) << 8; left = (int16_t)s0; if (g.movie.audio_channels >= 2) { int s1 = ((int)src[1] - 128) << 8; right = (int16_t)s1; } else { right = left; } } else { left = (int16_t)((uint16_t)src[0] | ((uint16_t)src[1] << 8)); if (g.movie.audio_channels >= 2) { right = (int16_t)((uint16_t)src[2] | ((uint16_t)src[3] << 8)); } else { right = left; } } g.audio_buf[g.audio_buf_len + 0] = (uint8_t)(left & 0xFF); g.audio_buf[g.audio_buf_len + 1] = (uint8_t)((left >> 8) & 0xFF); g.audio_buf[g.audio_buf_len + 2] = (uint8_t)(right & 0xFF); g.audio_buf[g.audio_buf_len + 3] = (uint8_t)((right >> 8) & 0xFF); g.audio_buf_len += 4; } g.audio_chunk_pos += read_size; } } static void feed_audio() { if (g.play_state != PlayState::Playing || g.audio_handle < 0) return; update_audio_hw_position(); for (int iter = 0; iter < 4; iter++) { if (g.audio_buf_len > 0) { int written = montauk::audio_write(g.audio_handle, g.audio_buf + g.audio_buf_pos, g.audio_buf_len); if (written <= 0) return; g.audio_buf_pos += written; g.audio_buf_len -= written; g.audio_output_written_bytes += (uint64_t)written; continue; } fill_audio_buffer(); if (g.audio_buf_len <= 0) return; } } static bool begin_playback_from_time(uint64_t time_us) { if (!g.movie.valid) return false; uint64_t duration = media_duration_us(); if (duration == 0) time_us = 0; else if (time_us > duration) time_us = duration; close_audio_session(); g.base_time_us = time_us; g.play_state = PlayState::Playing; sync_frame_to_time(time_us); if (movie_has_supported_audio()) { seek_audio_to_time(time_us); g.audio_handle = montauk::audio_open(g.movie.audio_rate, AUDIO_OUT_CHANNELS, AUDIO_OUT_BITS); if (g.audio_handle >= 0) { g.audio_hw_prev_pos = montauk::audio_get_pos(g.audio_handle); if (g.audio_hw_prev_pos < 0) g.audio_hw_prev_pos = 0; g.audio_hw_prev_pos %= AUDIO_RING_BYTES; g.audio_hw_valid = true; } else { set_status("Audio device unavailable; continuing without sound"); g.play_start_ms = montauk::get_milliseconds(); } } else { g.play_start_ms = montauk::get_milliseconds(); } return true; } static void pause_playback() { if (g.play_state != PlayState::Playing || !g.movie.valid) return; g.base_time_us = current_time_us(); close_audio_session(); g.play_state = PlayState::Paused; sync_frame_to_time(g.base_time_us); } static void stop_playback(bool keep_last_frame) { if (!g.movie.valid) { close_audio_session(); g.play_state = PlayState::Stopped; g.base_time_us = 0; return; } close_audio_session(); g.play_state = PlayState::Stopped; g.base_time_us = keep_last_frame ? media_duration_us() : 0; sync_frame_to_time(g.base_time_us); } static void seek_to_time(uint64_t time_us) { if (!g.movie.valid) return; uint64_t duration = media_duration_us(); if (duration > 0 && time_us > duration) time_us = duration; if (g.play_state == PlayState::Playing) { begin_playback_from_time(time_us); } else { close_audio_session(); g.base_time_us = time_us; sync_frame_to_time(time_us); } } static bool load_video(int index) { if (index < 0 || index >= g.file_count) return false; unload_movie(); char path[MAX_PATH * 2]; snprintf(path, sizeof(path), "%s/%s", g.dir_path, g.files[index].name); int fd = montauk::open(path); if (fd < 0) { set_status("Could not open AVI file"); return false; } uint64_t file_size = montauk::getsize(fd); if (file_size == 0 || file_size > MAX_FILE_SIZE) { montauk::close(fd); set_status("AVI file is empty or too large"); return false; } if (!avi::parse(g.movie, fd, file_size)) { char msg[128]; snprintf(msg, sizeof(msg), "AVI parse failed: %s", g.movie.error[0] ? g.movie.error : "unsupported file"); unload_movie(); set_status(msg); return false; } uint64_t pixels_size = (uint64_t)g.movie.width * (uint64_t)g.movie.height * 4ull; g.frame_pixels = (uint32_t*)montauk::malloc(pixels_size); if (!g.frame_pixels) { unload_movie(); set_status("Out of memory for video frame buffer"); return false; } montauk::memset(g.frame_pixels, 0, pixels_size); g.current_video = index; g.selected_item = index; g.displayed_frame_index = -1; g.base_time_us = 0; g.play_state = PlayState::Stopped; sync_frame_to_time(0); if (movie_has_supported_audio()) { char fps[32]; format_fps(fps, sizeof(fps)); snprintf(g.status_text, sizeof(g.status_text), "%s %dx%d %s PCM %u Hz", avi::codec_name(g.movie), g.movie.width, g.movie.height, fps, g.movie.audio_rate); } else if (g.movie.audio_chunk_count > 0) { char fps[32]; format_fps(fps, sizeof(fps)); snprintf(g.status_text, sizeof(g.status_text), "%s %dx%d %s audio unsupported", avi::codec_name(g.movie), g.movie.width, g.movie.height, fps); } else { char fps[32]; format_fps(fps, sizeof(fps)); snprintf(g.status_text, sizeof(g.status_text), "%s %dx%d %s silent", avi::codec_name(g.movie), g.movie.width, g.movie.height, fps); } return true; } static bool start_video(int index) { if (g.current_video != index || !g.movie.valid) { if (!load_video(index)) return false; } return begin_playback_from_time(0); } static bool play_selected_or_current() { if (g.selected_item >= 0 && filtered_contains(g.selected_item)) { if (g.current_video == g.selected_item && g.movie.valid) { uint64_t duration = media_duration_us(); uint64_t start = g.base_time_us; if (duration > 0 && start >= duration) start = 0; return begin_playback_from_time(start); } return start_video(g.selected_item); } if (g.current_video >= 0 && g.movie.valid) { uint64_t duration = media_duration_us(); uint64_t start = g.base_time_us; if (duration > 0 && start >= duration) start = 0; return begin_playback_from_time(start); } if (g.filtered_count > 0) return start_video(g.filtered_indices[0]); return false; } static bool next_video(bool auto_advanced = false) { if (g.file_count == 0) return false; if (g.current_video < 0) return start_video(0); if (auto_advanced && g.loop_enabled) return begin_playback_from_time(0); int next = g.current_video + 1; if (next >= g.file_count) { if (g.loop_enabled) next = 0; else { stop_playback(true); return false; } } return start_video(next); } static void prev_video() { if (g.file_count == 0) return; if (g.current_video >= 0 && current_time_us() > 3000000ull) { begin_playback_from_time(0); return; } int prev = g.current_video - 1; if (prev < 0) prev = g.file_count - 1; start_video(prev); } static uint64_t progress_time_from_x(int mx) { Rect bar = progress_rect(); if (bar.w <= 0) return 0; int rel = mx - bar.x; if (rel < 0) rel = 0; if (rel > bar.w) rel = bar.w; uint64_t duration = media_duration_us(); return (duration == 0) ? 0 : ((uint64_t)rel * duration) / (uint64_t)bar.w; } static void blit_scaled_argb(uint32_t* dst, int dw, int dh, const Rect& rect, const uint32_t* src, int sw, int sh) { if (!dst || !src || rect.w <= 0 || rect.h <= 0 || sw <= 0 || sh <= 0) return; for (int y = 0; y < rect.h; y++) { int dy = rect.y + y; if (dy < 0 || dy >= dh) continue; int sy = (int)((uint64_t)y * (uint64_t)sh / (uint64_t)rect.h); const uint32_t* src_row = src + (uint64_t)sy * (uint64_t)sw; for (int x = 0; x < rect.w; x++) { int dx = rect.x + x; if (dx < 0 || dx >= dw) continue; int sx = (int)((uint64_t)x * (uint64_t)sw / (uint64_t)rect.w); dst[dy * dw + dx] = src_row[sx]; } } } static void render_video_panel(uint32_t* pixels, const Rect& panel) { Rect inner = panel; px_fill(pixels, g.win_w, g.win_h, inner.x, inner.y, inner.w, inner.h, VIDEO_BG); if (g.movie.valid && g.frame_pixels) { int avail_w = inner.w; int avail_h = inner.h; int draw_w = avail_w; int draw_h = (int)((uint64_t)avail_w * (uint64_t)g.movie.height / (uint64_t)g.movie.width); if (draw_h > avail_h) { draw_h = avail_h; draw_w = (int)((uint64_t)avail_h * (uint64_t)g.movie.width / (uint64_t)g.movie.height); } if (draw_w < 1) draw_w = 1; if (draw_h < 1) draw_h = 1; Rect dst = { inner.x + (inner.w - draw_w) / 2, inner.y + (inner.h - draw_h) / 2, draw_w, draw_h }; px_fill(pixels, g.win_w, g.win_h, inner.x, inner.y, inner.w, inner.h, VIDEO_BG); blit_scaled_argb(pixels, g.win_w, g.win_h, dst, g.frame_pixels, g.movie.width, g.movie.height); } else { const char* line1 = "No file"; int y = inner.y + inner.h / 2 - font_h(FONT_SIZE_LG); px_text(pixels, g.win_w, g.win_h, inner.x + (inner.w - text_w(line1, FONT_SIZE_LG)) / 2, y, line1, TEXT, FONT_SIZE_LG); } px_hline(pixels, g.win_w, g.win_h, panel.x, panel.y + panel.h - 1, panel.w, BORDER); } static void render_progress(uint32_t* pixels) { int y = g.win_h - TRANSPORT_H - PROGRESS_H; px_fill(pixels, g.win_w, g.win_h, 0, y, g.win_w, PROGRESS_H, PANEL_BG_ALT); Rect bar = progress_rect(); px_fill_rounded(pixels, g.win_w, g.win_h, bar.x, bar.y, bar.w, bar.h, 3, TRACK_BG); uint64_t current = current_time_us(); uint64_t total = media_duration_us(); if (total > 0) { int fill = (int)((current * (uint64_t)bar.w) / total); if (fill > bar.w) fill = bar.w; if (fill > 0) px_fill_rounded(pixels, g.win_w, g.win_h, bar.x, bar.y, fill, bar.h, 3, ACCENT); px_circle(pixels, g.win_w, g.win_h, bar.x + fill, bar.y + bar.h / 2, 5, ACCENT); px_circle(pixels, g.win_w, g.win_h, bar.x + fill, bar.y + bar.h / 2, 2, WHITE); } char cur_str[24]; char total_str[24]; format_time(cur_str, sizeof(cur_str), current); format_time(total_str, sizeof(total_str), total); px_text(pixels, g.win_w, g.win_h, 18, y + 4, cur_str, TEXT_DIM, FONT_SIZE_SM); int tw0 = text_w(total_str, FONT_SIZE_SM); px_text(pixels, g.win_w, g.win_h, g.win_w - tw0 - 18, y + 4, total_str, TEXT_DIM, FONT_SIZE_SM); } static void render_transport(uint32_t* pixels) { int y = g.win_h - TRANSPORT_H; px_fill(pixels, g.win_w, g.win_h, 0, y, g.win_w, TRANSPORT_H, BG_BOTTOM); int bx, by, bw, bh, gap; transport_layout(&bx, &by, &bw, &bh, &gap); px_icon_button(pixels, g.win_w, g.win_h, bx, by, bw, bh, g.ico_rewind, BTN_BG, 8); bx += bw + gap; { bool active = (g.play_state == PlayState::Playing); px_icon_button(pixels, g.win_w, g.win_h, bx, by, bw, bh, active ? g.ico_pause_w : g.ico_play, active ? BTN_ACTIVE : BTN_BG, 8); } bx += bw + gap; { bool active = (g.current_video >= 0 && g.movie.valid); px_icon_button(pixels, g.win_w, g.win_h, bx, by, bw, bh, active ? g.ico_stop_w : g.ico_stop, active ? BTN_STOP : BTN_BG, 8); } bx += bw + gap; px_icon_button(pixels, g.win_w, g.win_h, bx, by, bw, bh, g.ico_forward, BTN_BG, 8); Rect repeat = repeat_button_rect(); px_icon_button(pixels, g.win_w, g.win_h, repeat.x, repeat.y, repeat.w, repeat.h, g.loop_enabled ? g.ico_repeat_w : g.ico_repeat, g.loop_enabled ? ACCENT : BTN_BG, 8); } static void render(uint32_t* pixels) { px_vgradient(pixels, g.win_w, g.win_h, 0, 0, g.win_w, g.win_h, BG_TOP, BG_BOTTOM); render_video_panel(pixels, video_rect()); render_progress(pixels); render_transport(pixels); } static bool tick_playback() { if (g.play_state != PlayState::Playing || !g.movie.valid) return false; feed_audio(); uint64_t now_ms = montauk::get_milliseconds(); uint64_t t = current_time_us(); uint64_t total = media_duration_us(); sync_frame_to_time(t); if (g.audio_handle >= 0 && g.audio_output_total_bytes > 0 && g.audio_hw_total_bytes >= g.audio_output_total_bytes) { if (total > 0 && t < total) { close_audio_session(); g.base_time_us = t; g.play_start_ms = now_ms; } else { next_video(true); return true; } } if (total > 0 && current_time_us() >= total) { next_video(true); return true; } return false; } static bool handle_click(int mx, int my) { if (repeat_button_rect().contains(mx, my)) { g.loop_enabled = !g.loop_enabled; return true; } Rect bar = progress_rect(); if (my >= bar.y - 8 && my < bar.y + bar.h + 8 && g.movie.valid) { g.dragging_progress = true; g.drag_time_us = progress_time_from_x(mx); sync_frame_to_time(g.drag_time_us); return true; } int bx, by, bw, bh, gap; transport_layout(&bx, &by, &bw, &bh, &gap); Rect prev = {bx, by, bw, bh}; Rect play = {bx + bw + gap, by, bw, bh}; Rect stop = {bx + (bw + gap) * 2, by, bw, bh}; Rect next = {bx + (bw + gap) * 3, by, bw, bh}; if (prev.contains(mx, my)) { prev_video(); return true; } if (play.contains(mx, my)) { if (g.play_state == PlayState::Playing) pause_playback(); else play_selected_or_current(); return true; } if (stop.contains(mx, my)) { stop_playback(false); return true; } if (next.contains(mx, my)) { next_video(false); return true; } return false; } static bool handle_key(const Montauk::KeyEvent& key, bool& quit) { if (!key.pressed) return false; if (key.scancode == 0x01) { quit = true; return false; } if (key.ascii == ' ') { if (g.play_state == PlayState::Playing) pause_playback(); else play_selected_or_current(); return true; } if (key.ascii == 's' || key.ascii == 'S') { stop_playback(false); return true; } if (key.ascii == 'l' || key.ascii == 'L') { g.loop_enabled = !g.loop_enabled; return true; } if (key.ascii == 'n' || key.ascii == 'N') { next_video(false); return true; } if (key.ascii == 'p' || key.ascii == 'P') { prev_video(); return true; } if (key.scancode == 0x4B) { uint64_t cur = current_time_us(); if (cur > 5000000ull) cur -= 5000000ull; else cur = 0; seek_to_time(cur); return true; } if (key.scancode == 0x4D) { seek_to_time(current_time_us() + 5000000ull); return true; } return false; } static void set_default_dir() { char username[32] = {}; if (montauk::getuser(username, sizeof(username)) > 0 && username[0]) { snprintf(g.dir_path, sizeof(g.dir_path), "0:/users/%s/Videos", username); const char* probe[1]; int ok = montauk::readdir(g.dir_path, probe, 1); if (ok < 0) { snprintf(g.dir_path, sizeof(g.dir_path), "0:/users/%s", username); } } else { montauk::strncpy(g.dir_path, "0:/home", (int)sizeof(g.dir_path)); } } static void parse_args(char* auto_file, int auto_file_cap) { char args[512] = {}; int arglen = montauk::getargs(args, sizeof(args)); if (arglen <= 0 || !args[0]) { set_default_dir(); return; } if (str_ends_with(args, ".avi")) { int last_slash = -1; for (int i = 0; args[i]; i++) { if (args[i] == '/') last_slash = i; } if (last_slash > 0) { int len = last_slash; if (len >= (int)sizeof(g.dir_path)) len = sizeof(g.dir_path) - 1; montauk::memcpy(g.dir_path, args, len); g.dir_path[len] = '\0'; montauk::strncpy(auto_file, args + last_slash + 1, auto_file_cap); } else { set_default_dir(); montauk::strncpy(auto_file, args, auto_file_cap); } } else { montauk::strncpy(g.dir_path, args, (int)sizeof(g.dir_path)); } } extern "C" void _start() { montauk::memset(&g, 0, sizeof(g)); g.win_w = INIT_W; g.win_h = INIT_H; g.movie.fd = -1; g.current_video = -1; g.selected_item = -1; g.hovered_item = -1; g.audio_handle = -1; g.displayed_frame_index = -1; g.play_state = PlayState::Stopped; set_status("Open an AVI from your Videos folder"); char auto_file[128] = {}; parse_args(auto_file, (int)sizeof(auto_file)); TrueTypeFont* font = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); if (font) { montauk::memset(font, 0, sizeof(TrueTypeFont)); if (!font->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(font); font = nullptr; } } g.font = font; Color icon_fg = TEXT; g.ico_rewind = svg_load("0:/icons/media-rewind.svg", 16, 16, icon_fg); g.ico_play = svg_load("0:/icons/media-play.svg", 16, 16, icon_fg); g.ico_pause = svg_load("0:/icons/media-pause.svg", 16, 16, icon_fg); g.ico_stop = svg_load("0:/icons/media-stop.svg", 16, 16, icon_fg); g.ico_forward = svg_load("0:/icons/media-forward.svg", 16, 16, icon_fg); g.ico_play_w = svg_load("0:/icons/media-play.svg", 16, 16, WHITE); g.ico_pause_w = svg_load("0:/icons/media-pause.svg", 16, 16, WHITE); g.ico_stop_w = svg_load("0:/icons/media-stop.svg", 16, 16, WHITE); g.ico_repeat = svg_load("0:/icons/media-playlist-repeat.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, icon_fg); g.ico_repeat_w = svg_load("0:/icons/media-playlist-repeat.svg", TOOL_ICON_SIZE, TOOL_ICON_SIZE, WHITE); scan_directory(); int auto_play_idx = -1; if (auto_file[0]) { for (int i = 0; i < g.file_count; i++) { if (str_eq_case_insensitive(g.files[i].name, auto_file)) { auto_play_idx = i; break; } } } WsWindow win; if (!win.create("Videos", INIT_W, INIT_H)) montauk::exit(1); if (auto_play_idx >= 0) { g.selected_item = auto_play_idx; start_video(auto_play_idx); } render(win.pixels); win.present(); uint64_t last_draw = montauk::get_milliseconds(); while (true) { Montauk::WinEvent ev; int r = win.poll(&ev); if (r < 0) break; bool redraw = false; bool quit = false; if (r == 0) { if (g.play_state == PlayState::Playing) { if (tick_playback()) redraw = true; uint64_t now = montauk::get_milliseconds(); if (now - last_draw >= 16) redraw = true; } if (redraw) { render(win.pixels); win.present(); last_draw = montauk::get_milliseconds(); } montauk::sleep_ms(g.play_state == PlayState::Playing ? 4 : 16); continue; } if (ev.type == 3) break; if (ev.type == 2) { g.win_w = win.width; g.win_h = win.height; ensure_selected_visible(); redraw = true; } else if (ev.type == 0 && ev.key.pressed) { if (handle_key(ev.key, quit)) redraw = true; } else if (ev.type == 1) { bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); if (clicked) { if (handle_click(ev.mouse.x, ev.mouse.y)) redraw = true; } if (g.dragging_progress && (ev.mouse.buttons & 1)) { g.drag_time_us = progress_time_from_x(ev.mouse.x); sync_frame_to_time(g.drag_time_us); redraw = true; } if (released && g.dragging_progress) { uint64_t target = g.drag_time_us; g.dragging_progress = false; seek_to_time(target); redraw = true; } } if (quit) break; if (g.play_state == PlayState::Playing) { if (tick_playback()) redraw = true; } if (redraw) { render(win.pixels); win.present(); last_draw = montauk::get_milliseconds(); } } unload_movie(); if (g.font) montauk::mfree(g.font); win.destroy(); montauk::exit(0); }