1435 lines
45 KiB
C++
1435 lines
45 KiB
C++
/*
|
|
* main.cpp
|
|
* MontaukOS Videos app -- AVI playback with PCM audio
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include <montauk/syscall.h>
|
|
#include <montauk/string.h>
|
|
#include <montauk/heap.h>
|
|
#include <gui/gui.hpp>
|
|
#include <gui/standalone.hpp>
|
|
#include <gui/truetype.hpp>
|
|
#include <gui/svg.hpp>
|
|
|
|
extern "C" {
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
}
|
|
|
|
#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);
|
|
}
|