Skip to content

BlackVideo Mini Player v2.4.0 — Standalone Mode, Sprite Preview, LLM Captions, Whisper Fix, Progress %, Auto-Updater

Latest

Choose a tag to compare

@LoneStamp LoneStamp released this 13 Mar 02:41

✅ What's in This Release


v2 4 0

🖥️ Standalone Mode — Open Without a Video

The player can now be launched with no arguments and shows a welcome screen instead of exiting.

Before (v2.3.0):

blackvideo-player.exe          ← exits immediately or crashes
blackvideo-player.exe video    ← only way to use it

After (v2.4.0):

blackvideo-player.exe          ← opens welcome screen
  • Welcome screen renders inside the SDL2 window: large icon, "Open a video or drop a file here" prompt, and an Open File button in the centre — all drawn in C via ui_overlay.c
  • Drag and drop: drop any video file onto the window at any time (welcome screen or during playback) — handled via SDL_DROPFILE event, requires SDL_EventState(SDL_DROPFILE, SDL_ENABLE) at init
  • player.ads gains a No_Video state alongside Playing / Paused / Stopped
  • main.adb no longer exits on missing argument — passes "" to Player.Run which enters No_Video state
  • Right-click Open Video File and the Open button both work from the welcome screen

How it works in Ada + C:

-- player.ads
type Player_State is (No_Video, Stopped, Playing, Paused);

-- player.adb: Run() — no argument guard removed
if Video_File = "" then
   State := No_Video;
   UI_Overlay.Set_No_Video_Mode (True);
end if;
// ui_overlay.c: draw welcome screen when g_no_video = 1
void bv_ui_draw(...) {
    if (g_no_video) { draw_welcome_screen(ren, w, h); return; }
    ...
}

🖼️ Sprite Thumbnail Preview on Hover

Hovering the seek bar shows a small thumbnail of the video at that position, with a timestamp label beneath it.

How it works:

When a video opens, a background cmd.exe /c job (same WinExec pattern as Whisper) runs:

ffmpeg -i video.mp4 -vf "fps=1/10,scale=160:-1" -q:v 5
       build\cache\videoname_%04d.jpg

This extracts one frame every 10 seconds as 160px-wide JPEG thumbnails and stores them in build\cache\<video_hash>\. On a 90-minute film that is ~540 small JPEGs (~2–3 MB total), generated in roughly 10–20 seconds in the background.

Hover rendering:

  • ui_overlay.c tracks mouse X over the seek bar and computes hover_pos = duration * (mouse_x - bar_x) / bar_w
  • Frame index = hover_pos / 10 → loads the matching JPEG via SDL_image (or stb_image — no extra DLL)
  • Renders a 160×90 popup box above the scrubber with the frame and a timestamp label below it
  • Popup disappears when the mouse leaves the seek bar area

New files:

  • src/thumb_cache.ads / .adb — Ada module: hash video path, check cache dir, launch ffmpeg job, map hover position → JPEG filename
  • csrc/thumb_preview.c — C: SDL_Texture loading (via stb_image.h, header-only), draw popup box

Cache location: build\cache\<8-char-hash-of-video-path>\frame_%04d.jpg
Cleared by right-click → Clear Thumbnail Cache (new menu item).


⚡ Whisper Performance Fix — Why It Was So Slow

Root cause (the real problem):

whisper_bridge.adb's Generate() function calls C_System() (the C system() function) to run both ffmpeg and whisper-cli. C_System is fully synchronous — it blocks the calling thread until the command exits. But Launch_Whisper in player.adb calls it via WinExec with cmd.exe /c "ffmpeg ... && whisper-cli ...".

WinExec is nominally asynchronous but its behaviour with cmd.exe /c "..." on modern Windows is unreliable — the cmd shell can hold the process slot open or Windows Defender / real-time antivirus scans the newly created .wav and .srt files before releasing them, causing the job to appear stalled for minutes. Additionally, whisper-cli with the ggml-base model runs on CPU only and is genuinely slow: expect ~0.3× real-time on a mid-range CPU (a 60-second video takes ~3–5 minutes, not hours — something else is wrong).

The real culprits causing "hours":

  1. Windows Defender scans every .wav and .srt file whisper-cli writes. On some machines this causes 100% disk hold for the duration.
  2. WinExec + cmd.exe /c with a long quoted string — Windows has a 8191-char limit on cmd.exe command lines. If the video path is long, the command silently truncates and whisper-cli never starts — but WinExec returns success (≥ 32) and Whisper_Running stays True forever, showing "generating..." permanently.
  3. The ggml-base model is genuinely slow for long content. A 2-hour film can take 30–60 minutes on CPU.

Fixes in v2.4.0:

  • Replace WinExec with CreateProcess — proper async process launch with a PROCESS_INFORMATION handle we keep alive and poll with GetExitCodeProcess(hProcess, &code); code == STILL_ACTIVE. This is reliable on all Windows versions and not affected by the cmd.exe quoting issues.
  • Write a sentinel .done file — at the end of the whisper batch, echo done > <out>.done. Check_Whisper_Done polls for the .done file, not the .srt, so we know the SRT is fully written before loading it.
  • Path length guard — if the full command string exceeds 7500 chars, the batch is written to a temp .bat file in %TEMP% and CreateProcess runs cmd.exe /c that.bat instead. No truncation.
  • Timeout detection — if Whisper_Running has been true for > 30 minutes with no .done file, the status banner changes to Whisper: stalled? Check Defender / console and a [Whisper] TIMEOUT message is printed to console.
  • Better model adviceWHISPER_SETUP.md updated with the Windows Defender exclusion path recommendation.

New Ada/C additions:

-- player.adb
Whisper_H_Process  : System.Address := System.Null_Address;
Whisper_Start_Tick : Interfaces.C.unsigned := 0;

function CreateProcess (...) return Boolean
with Import, Convention => C, External_Name => "bv_create_process";

-- csrc/process_helpers.c
int bv_create_process(const char *cmd, HANDLE *hProcess_out);
int bv_process_still_running(HANDLE hProcess);
void bv_close_handle(HANDLE h);

📊 Whisper / LLM Progress Percentage

Instead of a static Whisper: generating..., the status banner now shows a live percentage:

Whisper: generating...  23%   [████████░░░░░░░░░░░░░░░░░░░░░░]

How whisper-cli outputs progress:
whisper-cli writes progress lines to stderr in the form:

whisper_print_progress_callback: progress = 23%

Implementation:

  • bv_create_process is replaced with bv_create_process_with_pipe which creates a read pipe on stderr of the child process (using CreatePipe + STARTUPINFO.hStdError = write_end)
  • Each frame, Check_Whisper_Progress calls PeekNamedPipe (non-blocking) to read any available stderr bytes, scans for the progress = NN% pattern, and calls UI_Overlay.Set_Whisper_Status("Whisper: generating... NN%")
  • The C overlay draws a mini progress bar beside the text in the banner

Same pipe approach applies to LLM providers that stream token output (see below).


🤖 LLM Cloud Caption Generation

Six cloud providers available from the context menu — right-click → Generate Captions (LLM) ▶ → sub-menu:

Provider Model used Notes
Claude (Anthropic) claude-sonnet-4-5 Audio via base64 or transcript
OpenAI gpt-4o-audio-preview Native audio input
Gemini (Google) gemini-1.5-pro Audio file upload
DeepSeek deepseek-chat Text transcript → SRT
Grok (xAI) grok-2 Text transcript → SRT

Architecture — two-step approach (no audio upload for text-only providers):

  1. Run whisper-cli locally for a fast draft transcript (text only, no SRT timing) — takes ~30 seconds for a 60s clip with ggml-tiny
  2. Send the transcript text + video metadata to the LLM with a prompt: "Convert this transcript to a properly timed SRT subtitle file. Duration: Xs. Speaker count: N."
  3. The LLM returns a formatted .srt which is auto-loaded into Track 2

For OpenAI and Gemini which support native audio, step 1 is skipped and the WAV is uploaded directly.

API key storage — %APPDATA%\BlackVideo\keys.cfg:

ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AIza...
DEEPSEEK_API_KEY=...
GROK_API_KEY=...

First use: context menu shows Set API Key for [Provider] — a small SDL2 text input overlay appears (drawn in C). Keys are written to keys.cfg and reused on subsequent sessions.

HTTP in Ada + C:

  • csrc/llm_client.c — uses WinINet (available on all Windows, no extra DLL) for HTTPS POST. No libcurl dependency.
  • src/llm_bridge.ads / .adb — Ada wrapper: Send_Request(Provider, Transcript, Duration) → returns raw SRT string
  • Streaming responses read via InternetReadFile loop, status banner updated with token count: Claude: generating... 142 tokens

New context menu layout:

Open Video File...
─────────────────────
Subtitle: Off
  Track 1 — filename.srt
  Track 2 — (empty)
  Track 3 — (empty)
─────────────────────
Generate Captions (Whisper)
Translate to English (Whisper)
─────────────────────
Generate Captions (LLM) ▶
  Claude (Anthropic)
  OpenAI
  Gemini
  DeepSeek
  Grok
─────────────────────
Clear Thumbnail Cache
─────────────────────
Check for Updates...

🔔 Auto-Updater — GitHub Release Check

On every launch, the player silently checks https://raw.githubusercontent.com/BlackBlazent/blackvideo-mini-player/main/latest.json using WinINet. If the version field is newer than the compiled-in version string, a notification badge appears on the menu button.

latest.json format (add to repo root):

{
  "version": "2.4.0",
  "release_url": "https://github.com/BlackBlazent/blackvideo-mini-player/releases/tag/v2.4.0",
  "download_url": "https://sourceforge.net/projects/blackvideo-mini-player/files/blackvideo-mini-player-v2.4.0.win.zip/download",
  "notes": "Standalone mode, sprite preview, LLM captions, Whisper fix"
}

Context menu → Check for Updates... shows a small SDL2 overlay:

┌─────────────────────────────────┐
│  ✓ Update available: v2.4.0     │
│  "Standalone mode, sprite..."   │
│  [Download]      [Later]        │
└─────────────────────────────────┘

Clicking Download opens the download_url in the default browser via ShellExecuteA.

How version comparison works:

-- src/updater.ads / .adb
function Is_Newer (Remote, Local : String) return Boolean;
-- Splits "2.4.0" → (2, 4, 0) and compares lexicographically.
-- Returns True if Remote > Local.
// csrc/llm_client.c — reused for the update check
// bv_http_get(url, out_buf, buf_size) → bytes read
// Uses WinINet: InternetOpen → InternetOpenUrl → InternetReadFile
// Same function used by LLM client, so no extra code.

On launch: check is done asynchronously via CreateThread so it never delays startup. Result is stored in a global; the menu badge is drawn on the next frame after the thread completes.


📁 New Files in This Release

File Purpose
src/thumb_cache.ads / .adb Video thumbnail strip extraction and cache management
src/llm_bridge.ads / .adb LLM provider API wrapper (Claude, OpenAI, Gemini, DeepSeek, Grok)
src/updater.ads / .adb GitHub release version check, semver comparison
csrc/thumb_preview.c SDL2 thumbnail popup rendering, stb_image.h JPEG load
csrc/process_helpers.c CreateProcess + pipe helpers replacing WinExec
csrc/llm_client.c WinINet HTTPS client (LLM requests + update check)
include/stb_image.h Single-header JPEG/PNG loader (no extra DLL)
latest.json Repo root — current version manifest for auto-updater
%APPDATA%\BlackVideo\keys.cfg Runtime-created API key storage (not in repo)

🔧 Modified Files

File Changes
src/player.adb No_Video state, SDL_DROPFILE, CreateProcess whisper launch, progress pipe polling, updater thread start, LLM dispatch
src/player.ads No_Video state in Player_State enum
src/main.adb Remove argument guard — pass "" on no args
src/ui_overlay.ads / .adb New C imports: welcome screen, thumbnail popup, update badge, LLM sub-menu, key input overlay
csrc/ui_overlay.c Welcome screen, thumbnail popup, progress bar in banner, update badge on menu button, LLM sub-menu, API key input overlay
src/whisper_bridge.adb Generate uses CreateProcess + pipe; sentinel .done file; path-length guard; timeout detection
scripts/build.bat Compile process_helpers.c, thumb_preview.c, llm_client.c; define STB_IMAGE_IMPLEMENTATION
scripts/build.sh Same for Linux/macOS
blackvideo_player.gpr Add new .o files to linker; add -lwininet for Windows

🎮 Updated Controls

Context Menu

Item Action
Open Video File... Open file dialog (works from welcome screen too)
Subtitle: Off / Track N Toggle, shows active track name
Track 1 / 2 / 3 Load or activate subtitle slot
Generate Captions (Whisper) Offline transcription with live % progress
Translate to English (Whisper) Offline translation with live % progress
Generate Captions (LLM) ▶ Sub-menu: Claude / OpenAI / Gemini / DeepSeek / Grok
Clear Thumbnail Cache Delete build\cache\ for this video
Check for Updates... Manual update check overlay

📦 New Requirements (Windows)

All existing DLL requirements from v2.3.0 remain. No new DLLs required:

  • stb_image.h is header-only — compiled directly into thumb_preview.c
  • WinINet is part of Windows (wininet.dll) — already present on all Windows 10/11
  • CreateProcess is part of kernel32.dll — no new import

New linker flag added to GPR: -lwininet


Built with Ada 2012 · FFmpeg 8.x · SDL2 · SDL2_ttf · whisper.cpp · WinINet · GNAT Community 2021

Full Changelog: v2.3.0...v2.4.0