From e0136bc7f4fc210b018fb95fc84e5d0b0ed5de72 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sat, 5 Sep 2026 11:34:23 -0300 Subject: [PATCH] ipod6g: add hardware H.264 video playback Add S5L8702 VPU-B initialization, reset, bitstream input, frame output, and cache maintenance to the iPod 6G target. Advertise it with HAVE_HW_H264 and append a capability-gated decoder interface to the plugin API. Keep MP4 parsing, AAC decode and mixer output, A/V synchronization, playback controls, resume state, and presentation in the multi-file h264_player plugin. The target layer exposes decoder operations only. Accept non-fragmented MP4/M4V containing Constrained Baseline H.264 through level 3.0, up to 640x480 at 30 fps, with optional AAC-LC audio. Validate codec configuration and all sample-table relationships before activating the hardware. Read MP4 tables in bulk, boost the CPU while preparing them, and report staged loading progress so long movies do not appear to hang during startup. Register the viewer and document its format limits and controls. The libm4a compatibility fixes needed by video-first containers remain in the preceding standalone change. Tested on an iPod Classic 6G through an isolated Rolo nightly runtime: H.264/AAC playback and M4V startup succeeded. Normal and isolated-runtime iPod 6G builds also complete, and git diff --check is clean. Change-Id: I1e96c65c7d0b4231a94f602f8052f1b26d6e4a80 --- apps/plugin.c | 6 + apps/plugin.h | 8 +- apps/plugins/SUBDIRS | 4 + apps/plugins/h264_player/SOURCES | 5 + apps/plugins/h264_player/h264_player.c | 51 + apps/plugins/h264_player/h264_player.make | 26 + apps/plugins/h264_player/mp4_demux.c | 1548 +++++++++++++++++ apps/plugins/h264_player/mp4_demux.h | 150 ++ apps/plugins/h264_player/video_audio.c | 563 ++++++ apps/plugins/h264_player/video_audio.h | 60 + apps/plugins/h264_player/video_pcm.c | 250 +++ apps/plugins/h264_player/video_pcm.h | 52 + apps/plugins/h264_player/video_playback.c | 1132 ++++++++++++ apps/plugins/h264_player/video_playback.h | 21 + apps/plugins/viewers.config | 5 + docs/CREDITS | 1 + firmware/SOURCES | 4 + firmware/export/config/ipod6g.h | 5 + firmware/export/hw_h264.h | 39 + firmware/export/s5l87xx.h | 22 + firmware/target/arm/s5l8702/ipod6g/vpu-6g.c | 71 + firmware/target/arm/s5l8702/ipod6g/vpu-6g.h | 48 + .../target/arm/s5l8702/ipod6g/vpu-h264-6g.c | 1081 ++++++++++++ .../target/arm/s5l8702/ipod6g/vpu-h264-6g.h | 84 + firmware/target/arm/s5l8702/system-s5l8702.c | 3 + manual/platform/ipod6g.tex | 1 + manual/plugins/h264_player.tex | 26 + manual/plugins/main.tex | 5 + 28 files changed, 5270 insertions(+), 1 deletion(-) create mode 100644 apps/plugins/h264_player/SOURCES create mode 100644 apps/plugins/h264_player/h264_player.c create mode 100644 apps/plugins/h264_player/h264_player.make create mode 100644 apps/plugins/h264_player/mp4_demux.c create mode 100644 apps/plugins/h264_player/mp4_demux.h create mode 100644 apps/plugins/h264_player/video_audio.c create mode 100644 apps/plugins/h264_player/video_audio.h create mode 100644 apps/plugins/h264_player/video_pcm.c create mode 100644 apps/plugins/h264_player/video_pcm.h create mode 100644 apps/plugins/h264_player/video_playback.c create mode 100644 apps/plugins/h264_player/video_playback.h create mode 100644 firmware/export/hw_h264.h create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-6g.c create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-6g.h create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c create mode 100644 firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h create mode 100644 manual/plugins/h264_player.tex diff --git a/apps/plugin.c b/apps/plugin.c index 51b4478421..2e747fca61 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -49,6 +49,9 @@ #include "language.h" #include "statusbar-skinned.h" #include "panic.h" +#ifdef HAVE_HW_H264 +#include "hw_h264.h" +#endif #if CONFIG_CHARGING #include "power.h" @@ -880,6 +883,9 @@ static const struct plugin_api rockbox_api = { gesture_flick_get_in_vp, gesture_flick_get, #endif +#ifdef HAVE_HW_H264 + &target_hw_h264_api, +#endif }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index cc10371a35..46e32d8b9b 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -83,6 +83,9 @@ int plugin_open(const char *plugin, const char *parameter); #include "metadata.h" #include "sound.h" #include "audio.h" +#ifdef HAVE_HW_H264 +#include "hw_h264.h" +#endif #include "voice_thread.h" #include "root_menu.h" #include "talk.h" @@ -179,7 +182,7 @@ int plugin_open(const char *plugin, const char *parameter); * when this happens please take the opportunity to sort in * any new functions "waiting" at the end of the list. */ -#define PLUGIN_API_VERSION 284 +#define PLUGIN_API_VERSION 285 /* 239 Marks the removal of ARCHOS HWCODEC and CHARCELL */ @@ -1046,6 +1049,9 @@ struct plugin_api { const struct viewport *vp); int (*gesture_flick_get)(const struct gesture_event *gevt); #endif +#ifdef HAVE_HW_H264 + const struct hw_h264_api *hw_h264; +#endif }; /* plugin header */ diff --git a/apps/plugins/SUBDIRS b/apps/plugins/SUBDIRS index b035fc9f50..7c88b086be 100644 --- a/apps/plugins/SUBDIRS +++ b/apps/plugins/SUBDIRS @@ -83,6 +83,10 @@ pdbox mpegplayer #endif +#ifdef HAVE_HW_H264 +h264_player +#endif + /* Lua needs at least 160 KB to work in */ #if PLUGIN_BUFFER_SIZE >= 0x80000 lua diff --git a/apps/plugins/h264_player/SOURCES b/apps/plugins/h264_player/SOURCES new file mode 100644 index 0000000000..0cf6cd667a --- /dev/null +++ b/apps/plugins/h264_player/SOURCES @@ -0,0 +1,5 @@ +mp4_demux.c +video_audio.c +video_pcm.c +video_playback.c +h264_player.c diff --git a/apps/plugins/h264_player/h264_player.c b/apps/plugins/h264_player/h264_player.c new file mode 100644 index 0000000000..d76bf72379 --- /dev/null +++ b/apps/plugins/h264_player/h264_player.c @@ -0,0 +1,51 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" +#include "video_playback.h" + +enum plugin_status plugin_start(const void *parameter) +{ + const char *path = parameter; + size_t buffer_size = 0; + void *buffer; + int result; + + if (path == NULL || path[0] == '\0') + { + rb->splash(HZ * 2, "No video file"); + return PLUGIN_ERROR; + } + + buffer = rb->plugin_get_audio_buffer(&buffer_size); + if (buffer == NULL || buffer_size == 0) + { + rb->splash(HZ * 2, "Not enough video memory"); + return PLUGIN_ERROR; + } + + result = video_h264_play(path, buffer, buffer_size); + rb->button_clear_queue(); + rb->plugin_release_audio_buffer(); + + if (result == 2) + return PLUGIN_USB_CONNECTED; + return result < 0 ? PLUGIN_ERROR : PLUGIN_OK; +} diff --git a/apps/plugins/h264_player/h264_player.make b/apps/plugins/h264_player/h264_player.make new file mode 100644 index 0000000000..ae0c2e0dd6 --- /dev/null +++ b/apps/plugins/h264_player/h264_player.make @@ -0,0 +1,26 @@ +# __________ __ ___. +# Open \______ \ ____ ____ | | _\_ |__ _______ ___ +# Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +# Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +# Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +# \/ \/ \/ \/ \/ +# + +H264PLAYERSRCDIR := $(APPSDIR)/plugins/h264_player +H264PLAYERBUILDDIR := $(BUILDDIR)/apps/plugins/h264_player + +ROCKS += $(H264PLAYERBUILDDIR)/h264_player.rock + +H264PLAYER_SRC := $(call preprocess, $(H264PLAYERSRCDIR)/SOURCES) +H264PLAYER_OBJ := $(call c2obj, $(H264PLAYER_SRC)) + +# add source files to OTHER_SRC to get automatic dependencies +OTHER_SRC += $(H264PLAYER_SRC) + +$(H264PLAYERBUILDDIR)/h264_player.rock: $(H264PLAYER_OBJ) + +$(H264PLAYERBUILDDIR)/%.o: $(H264PLAYERSRCDIR)/%.c \ + $(H264PLAYERSRCDIR)/h264_player.make + $(SILENT)mkdir -p $(dir $@) + $(call PRINTS,CC $(subst $(ROOTDIR)/,,$<))$(CC) -I$(dir $<) \ + $(PLUGINFLAGS) -c $< -o $@ diff --git a/apps/plugins/h264_player/mp4_demux.c b/apps/plugins/h264_player/mp4_demux.c new file mode 100644 index 0000000000..139b776bff --- /dev/null +++ b/apps/plugins/h264_player/mp4_demux.c @@ -0,0 +1,1548 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * MP4 video demuxer - extracts H.264 video track data from MP4/M4V files. + * Based on libm4a/demux.c box-walking pattern, with fd-based I/O and + * video track support (vmhd + avc1/avcC parsing). + * + * Original demux.c copyright: + * Copyright (c) 2005 David Hammerton + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "plugin.h" +#include "system.h" +#include "file.h" +#include "rbendian.h" +#include "mp4_demux.h" + +#include + +/* --- Simple fd-based stream I/O --- */ + +struct mp4_stream { + int fd; + off_t pos; + off_t size; + bool eof; +}; + +static void stream_init(struct mp4_stream *s, int fd, off_t size) +{ + s->fd = fd; + s->pos = 0; + s->size = size; + s->eof = false; +} + +static bool stream_read(struct mp4_stream *s, void *buf, size_t len) +{ + size_t done = 0; + + if (s->pos < 0 || s->pos > s->size || + len > (size_t)(s->size - s->pos)) + { + s->eof = true; + return false; + } + + while (done < len) + { + ssize_t count = rb->read(s->fd, (uint8_t *)buf + done, len - done); + + if (count <= 0) + { + s->eof = true; + return false; + } + done += (size_t)count; + s->pos += count; + } + return true; +} + +static uint32_t stream_read_uint32(struct mp4_stream *s) +{ + uint8_t buf[4]; + if (!stream_read(s, buf, 4)) + return 0; + /* big-endian */ + return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) + | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; +} + +static uint16_t stream_read_uint16(struct mp4_stream *s) +{ + uint8_t buf[2]; + if (!stream_read(s, buf, 2)) + return 0; + return ((uint16_t)buf[0] << 8) | (uint16_t)buf[1]; +} + +static uint8_t stream_read_uint8(struct mp4_stream *s) +{ + uint8_t b; + if (!stream_read(s, &b, 1)) + return 0; + return b; +} + +static bool stream_read_uint32_array(struct mp4_stream *s, uint32_t *values, + uint32_t count) +{ + uint32_t i; + + if (!stream_read(s, values, (size_t)count * sizeof(*values))) + return false; + for (i = 0; i < count; i++) + values[i] = betoh32(values[i]); + return true; +} + +static void stream_skip(struct mp4_stream *s, size_t n) +{ + if (s->pos < 0 || s->pos > s->size || + n > (size_t)(s->size - s->pos)) + { + s->eof = true; + return; + } + s->pos = rb->lseek(s->fd, (off_t)n, SEEK_CUR); + if (s->pos < 0) + s->eof = true; +} + +static off_t stream_tell(struct mp4_stream *s) +{ + return s->pos; +} + +/* --- Box parsers --- */ + +struct mp4_parse_ctx { + struct mp4_stream stream; + struct mp4v_demux_res *res; + bool found_video; /* set when we find a video track */ + bool found_audio; /* set when we find an audio track */ + bool in_video_trak; /* currently inside a video trak */ + bool in_audio_trak; /* currently inside an audio trak */ + uint16_t tkhd_dw; /* tkhd display width (pending video confirm) */ + uint16_t tkhd_dh; /* tkhd display height (pending video confirm) */ + /* Pending per-trak fields (committed at end of trak once type is known) */ + uint32_t pending_timescale; /* from mdhd (parsed before hdlr) */ + uint32_t pending_lead_trim; /* from edts/elst (parsed before mdia) */ +}; + +/* Parse avcC box - H.264 decoder configuration record. + * Layout: configurationVersion(1) + AVCProfileIndication(1) + + * profile_compatibility(1) + AVCLevelIndication(1) + + * lengthSizeMinusOne(1, lower 2 bits) + + * numSPS(1, lower 5 bits) + [spsLen(2)+spsData]... + + * numPPS(1) + [ppsLen(2)+ppsData]... */ +static bool read_chunk_avcc(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t data_len; + uint8_t config_ver; + + if (chunk_len < 8) + return false; + data_len = chunk_len - 8; + if (data_len > MP4V_MAX_CODECDATA || data_len < 7) + return false; + + /* Read the entire avcC payload into codecdata */ + ctx->res->codecdata_len = data_len; + if (!stream_read(&ctx->stream, ctx->res->codecdata, data_len)) + return false; + + /* Parse key fields from the buffer */ + config_ver = ctx->res->codecdata[0]; + if (config_ver != 1) + return false; + + ctx->res->avc_profile = ctx->res->codecdata[1]; + ctx->res->avc_level = ctx->res->codecdata[3]; + ctx->res->nalu_len_size = (ctx->res->codecdata[4] & 0x03) + 1; + ctx->res->num_sps = ctx->res->codecdata[5] & 0x1F; + + /* Count PPS: skip past SPS entries to find numPPS */ + { + size_t offset = 6; + uint8_t i; + + for (i = 0; i < ctx->res->num_sps && offset + 2 <= data_len; i++) + { + uint16_t sps_len = ((uint16_t)ctx->res->codecdata[offset] << 8) + | ctx->res->codecdata[offset + 1]; + offset += 2 + sps_len; + } + + if (offset < data_len) + ctx->res->num_pps = ctx->res->codecdata[offset]; + } + + return true; +} + +/* Parse stsd for video: expects avc1 sample entry */ +static bool read_chunk_stsd_video(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + /* version + flags */ + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + for (i = 0; i < numentries && size_remaining > 8; i++) + { + uint32_t entry_size = stream_read_uint32(&ctx->stream); + uint32_t format = stream_read_uint32(&ctx->stream); + uint32_t entry_remaining = entry_size - 8; + + if (entry_size <= 8 || entry_size > size_remaining) + return false; + + if (format == MAKEFOURCC('a','v','c','1') + || format == MAKEFOURCC('a','v','c','3')) + { + if (entry_remaining < 78) + return false; + ctx->res->format = format; + + /* reserved(6) + data_ref_index(2) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* pre-defined(2) + reserved(2) + pre-defined(12) */ + stream_skip(&ctx->stream, 16); + entry_remaining -= 16; + + ctx->res->width = stream_read_uint16(&ctx->stream); + ctx->res->height = stream_read_uint16(&ctx->stream); + entry_remaining -= 4; + + /* horiz_resolution(4) + vert_resolution(4) + reserved(4) + + frame_count(2) + compressor_name(32) + depth(2) + + pre_defined(2) */ + stream_skip(&ctx->stream, 50); + entry_remaining -= 50; + + /* Walk sub-boxes looking for avcC */ + while (entry_remaining > 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len <= 8 || sub_len > entry_remaining) + break; + + if (sub_id == MAKEFOURCC('a','v','c','C')) + { + if (!read_chunk_avcc(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + entry_remaining -= sub_len; + } + + if (entry_remaining > 0) + stream_skip(&ctx->stream, entry_remaining); + } + else + { + /* Unknown video format, skip */ + stream_skip(&ctx->stream, entry_remaining); + } + + size_remaining -= entry_size; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return ctx->res->format != 0; +} + +/* Read MPEG-4 descriptor length (variable length, 7 bits per byte, + * high bit means continue). + * Ported from libm4a/demux.c:78-92. */ +static uint32_t read_mp4_descr_length(struct mp4_stream *s) +{ + uint8_t b; + uint8_t num_bytes = 0; + uint32_t length = 0; + + do { + b = stream_read_uint8(s); + num_bytes++; + length = (length << 7) | (b & 0x7F); + } while ((b & 0x80) && num_bytes < 4); + + return length; +} + +/* Parse esds box - extract AudioSpecificConfig for AAC decoder init. + * Ported from libm4a/demux.c:95-154. */ +static bool read_chunk_esds(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + off_t box_end; + uint8_t tag; + uint32_t descr_len; + + if (chunk_len < 12) + return false; + box_end = ctx->stream.pos + (off_t)(chunk_len - 8); + + /* version + flags */ + stream_read_uint32(&ctx->stream); + + /* ES_Descriptor (tag 0x03) */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + (void)descr_len; + + if (tag == 0x03) + { + /* ES_ID(2) + streamPriority(1) */ + stream_skip(&ctx->stream, 3); + } + else + { + /* Non-standard: skip 2 bytes instead */ + stream_skip(&ctx->stream, 2); + } + + /* DecoderConfigDescriptor (tag 0x04) */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + (void)descr_len; + + if (tag != 0x04) + { + off_t remaining = box_end - ctx->stream.pos; + + if (remaining < 0) + return false; + stream_skip(&ctx->stream, (size_t)remaining); + return !ctx->stream.eof; + } + + /* objectTypeIndication(1) + streamType(1) + bufferSizeDB(3) + + maxBitrate(4) + avgBitrate(4) = 13 bytes */ + stream_skip(&ctx->stream, 13); + + /* DecoderSpecificInfo (tag 0x05) = AudioSpecificConfig */ + tag = stream_read_uint8(&ctx->stream); + descr_len = read_mp4_descr_length(&ctx->stream); + + if (tag == 0x05 && descr_len > 0) + { + if (descr_len > sizeof(ctx->res->audio_codecdata)) + descr_len = sizeof(ctx->res->audio_codecdata); + + ctx->res->audio_codecdata_len = descr_len; + stream_read(&ctx->stream, ctx->res->audio_codecdata, descr_len); + } + + /* Skip any trailing descriptors (SLConfigDescriptor etc.) */ + { + off_t remaining = box_end - ctx->stream.pos; + if (remaining > 0) + stream_skip(&ctx->stream, (size_t)remaining); + } + + return !ctx->stream.eof && ctx->stream.pos == box_end; +} + +/* Parse stsd for audio: expects mp4a sample entry. + * mp4a layout: reserved(6) + data_ref_index(2) + reserved(8) + + * channels(2) + sample_size(2) + packet_size(2) + sample_rate(4, 16.16 FP) + + * reserved(2) + sub-boxes (esds, etc.) */ +static bool read_chunk_stsd_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + for (i = 0; i < numentries && size_remaining > 8; i++) + { + uint32_t entry_size = stream_read_uint32(&ctx->stream); + uint32_t format = stream_read_uint32(&ctx->stream); + uint32_t entry_remaining = entry_size - 8; + + if (entry_size <= 8 || entry_size > size_remaining) + return false; + + if (format == MAKEFOURCC('m','p','4','a')) + { + if (entry_remaining < 28) + return false; + ctx->res->audio_format = format; + + /* reserved(6) + data_ref_index(2) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* reserved(8) */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* channelCount(2) + sampleSize(2) + compressionID(2) + + packetSize(2) = 8 bytes */ + stream_skip(&ctx->stream, 8); + entry_remaining -= 8; + + /* sampleRate: 16.16 fixed-point, 4 bytes */ + { + uint32_t sr_fp = stream_read_uint32(&ctx->stream); + ctx->res->audio_sample_rate = sr_fp >> 16; + } + entry_remaining -= 4; + + /* Walk sub-boxes for esds */ + while (entry_remaining > 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len <= 8 || sub_len > entry_remaining) + break; + + if (sub_id == MAKEFOURCC('e','s','d','s')) + { + if (!read_chunk_esds(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + entry_remaining -= sub_len; + } + + if (entry_remaining > 0) + stream_skip(&ctx->stream, entry_remaining); + } + else + { + stream_skip(&ctx->stream, entry_remaining); + } + + size_remaining -= entry_size; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +static bool read_chunk_stsz_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t default_size; + uint32_t numsizes; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + default_size = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numsizes = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (default_size == 0 && numsizes > size_remaining / 4) + return false; + ctx->res->audio_num_samples = numsizes; + stream_skip(&ctx->stream, size_remaining); + return !ctx->stream.eof; +} + +static bool read_audio_table_count(struct mp4_parse_ctx *ctx, + size_t chunk_len, size_t entry_size, + uint32_t *count) +{ + size_t size_remaining; + uint32_t numentries; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); + size_remaining -= 4; + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + if (entry_size == 0 || numentries > size_remaining / entry_size) + return false; + *count = numentries; + stream_skip(&ctx->stream, size_remaining); + return !ctx->stream.eof; +} + +static bool read_chunk_stsc_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 12, + &ctx->res->audio_num_stsc); +} + +static bool read_chunk_stco_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 4, + &ctx->res->audio_num_stco); +} + +static bool read_chunk_co64_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + return read_audio_table_count(ctx, chunk_len, 8, + &ctx->res->audio_num_stco); +} + +/* Audio sample table container */ +static bool read_chunk_stbl_audio(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('s','t','s','d'): + if (!read_chunk_stsd_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','z'): + if (!read_chunk_stsz_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','c'): + if (!read_chunk_stsc_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','c','o'): + if (!read_chunk_stco_audio(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('c','o','6','4'): + if (!read_chunk_co64_audio(ctx, sub_len)) + return false; + break; + /* No stss for audio; all AAC frames are sync samples */ + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* stts - time-to-sample table */ +static bool read_chunk_stts(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > MP4V_MAX_STTS || numentries > size_remaining / 8) + return false; + + ctx->res->num_stts = numentries; + if (!stream_read(&ctx->stream, ctx->res->stts, + (size_t)numentries * sizeof(ctx->res->stts[0]))) + return false; + size_remaining -= (size_t)numentries * 8; + for (i = 0; i < numentries; i++) + { + ctx->res->stts[i].sample_count = + betoh32(ctx->res->stts[i].sample_count); + ctx->res->stts[i].sample_delta = + betoh32(ctx->res->stts[i].sample_delta); + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stsz - sample sizes */ +static bool read_chunk_stsz(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t default_size, numsizes, i; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + default_size = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + numsizes = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (default_size == 0 && numsizes > size_remaining / 4) + return false; + ctx->res->num_samples = numsizes; + + if (default_size != 0) + { + /* All samples same size */ + uint32_t cap = ctx->res->sample_sizes == NULL ? 0 : + MIN(numsizes, ctx->res->sample_sizes_cap); + for (i = 0; i < cap; i++) + ctx->res->sample_sizes[i] = default_size; + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + return true; + } + + /* Variable sample sizes */ + { + uint32_t cap = ctx->res->sample_sizes == NULL ? 0 : + MIN(numsizes, ctx->res->sample_sizes_cap); + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->sample_sizes, cap)) + return false; + size_remaining -= (size_t)cap * 4; + /* Skip remaining if we hit our cap */ + if (numsizes > cap) + { + size_t skip = (size_t)(numsizes - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stsc - sample-to-chunk table */ +static bool read_chunk_stsc(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i, fill; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 12) + return false; + ctx->res->num_stsc = numentries; + fill = ctx->res->stsc == NULL ? 0 : + MIN(numentries, ctx->res->stsc_cap); + + if (!stream_read(&ctx->stream, ctx->res->stsc, + (size_t)fill * sizeof(ctx->res->stsc[0]))) + return false; + size_remaining -= (size_t)fill * 12; + for (i = 0; i < fill; i++) + { + ctx->res->stsc[i].first_chunk = + betoh32(ctx->res->stsc[i].first_chunk); + ctx->res->stsc[i].samples_per_chunk = + betoh32(ctx->res->stsc[i].samples_per_chunk); + ctx->res->stsc[i].sample_desc_index = + betoh32(ctx->res->stsc[i].sample_desc_index); + } + + if (numentries > fill) + { + size_t skip = (size_t)(numentries - fill) * 12; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stco - chunk offset table */ +static bool read_chunk_stco(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 4) + return false; + ctx->res->num_stco = numentries; /* real count from file */ + cap = ctx->res->chunk_offsets == NULL ? 0 : + MIN(numentries, ctx->res->chunk_offsets_cap); + + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->chunk_offsets, cap)) + return false; + size_remaining -= (size_t)cap * 4; + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* co64 - 64-bit chunk offset table (for files >4GB). + * Reads 64-bit offsets but truncates to 32-bit. + * Files with chunk offsets >4GB are not supported. */ +static bool read_chunk_co64(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries, i, cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 8) + return false; + ctx->res->num_stco = numentries; /* real count from file */ + cap = ctx->res->chunk_offsets == NULL ? 0 : + MIN(numentries, ctx->res->chunk_offsets_cap); + + for (i = 0; i < cap; i++) + { + uint32_t hi = stream_read_uint32(&ctx->stream); + uint32_t lo = stream_read_uint32(&ctx->stream); + if (hi != 0) + return false; + ctx->res->chunk_offsets[i] = lo; + size_remaining -= 8; + } + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 8; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stss - sync sample (keyframe) table */ +static bool read_chunk_stss(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t numentries; + uint32_t cap; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + numentries = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + if (numentries > size_remaining / 4) + return false; + ctx->res->num_stss = numentries; + cap = ctx->res->sync_samples == NULL ? 0 : + MIN(numentries, ctx->res->sync_samples_cap); + + if (!stream_read_uint32_array(&ctx->stream, + ctx->res->sync_samples, cap)) + return false; + size_remaining -= (size_t)cap * 4; + + if (numentries > cap) + { + size_t skip = (size_t)(numentries - cap) * 4; + + stream_skip(&ctx->stream, skip); + size_remaining -= skip; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* stbl - sample table container */ +static bool read_chunk_stbl(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('s','t','s','d'): + if (!read_chunk_stsd_video(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','t','s'): + if (!read_chunk_stts(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','z'): + if (!read_chunk_stsz(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','c'): + if (!read_chunk_stsc(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','c','o'): + if (!read_chunk_stco(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('c','o','6','4'): + if (!read_chunk_co64(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('s','t','s','s'): + if (!read_chunk_stss(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* minf - accepts vmhd (video) instead of smhd (audio) */ +static bool read_chunk_minf(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + /* Walk sub-boxes, look for vmhd to confirm video, then find stbl */ + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('v','m','h','d'): + /* Video media header confirms this is a video track */ + ctx->in_video_trak = true; + stream_skip(&ctx->stream, sub_len - 8); + break; + case MAKEFOURCC('s','m','h','d'): + /* Sound media header confirms this is an audio track */ + ctx->in_video_trak = false; + ctx->in_audio_trak = true; + stream_skip(&ctx->stream, sub_len - 8); + break; + case MAKEFOURCC('s','t','b','l'): + if (ctx->in_video_trak) + { + if (!read_chunk_stbl(ctx, sub_len)) + return false; + } + else if (ctx->in_audio_trak) + { + if (!read_chunk_stbl_audio(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* mdhd - media header (extract timescale) */ +static bool read_chunk_mdhd(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version; + + if (chunk_len < 28) + return false; + size_remaining = chunk_len - 8; + version = stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + { + uint32_t ts; + + if ((version >> 24) == 0) + { + /* Version 0: 4-byte fields */ + stream_skip(&ctx->stream, 4); /* creation_time */ + stream_skip(&ctx->stream, 4); /* modification_time */ + ts = stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* duration */ + size_remaining -= 16; + } + else if ((version >> 24) == 1) + { + if (chunk_len < 40) + return false; + /* Version 1: 8-byte fields */ + stream_skip(&ctx->stream, 8); /* creation_time */ + stream_skip(&ctx->stream, 8); /* modification_time */ + ts = stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 8); /* duration */ + size_remaining -= 28; + } + else + { + return false; + } + + /* Save pending; commit at end of trak once type is known. + * mdhd comes before hdlr in mdia, so track type isn't known yet. */ + ctx->pending_timescale = ts; + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* hdlr - handler reference (check for 'vide') */ +static bool read_chunk_hdlr(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t handler_type; + + if (chunk_len < 20) + return false; + size_remaining = chunk_len - 8; + stream_read_uint32(&ctx->stream); /* version + flags */ + stream_read_uint32(&ctx->stream); /* pre_defined */ + handler_type = stream_read_uint32(&ctx->stream); + size_remaining -= 12; + + ctx->in_video_trak = false; + ctx->in_audio_trak = false; + + if (handler_type == MAKEFOURCC('v','i','d','e')) + ctx->in_video_trak = true; + else if (handler_type == MAKEFOURCC('s','o','u','n')) + ctx->in_audio_trak = true; + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* mdia - media container */ +static bool read_chunk_mdia(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('m','d','h','d'): + if (!read_chunk_mdhd(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('h','d','l','r'): + if (!read_chunk_hdlr(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('m','i','n','f'): + if (!read_chunk_minf(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* tkhd - track header (extract display width/height for PAR) */ +static bool read_chunk_tkhd(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version; + uint32_t dw_fp, dh_fp; /* 16.16 fixed-point display dimensions */ + + if (chunk_len < 92) + return false; + size_remaining = chunk_len - 8; + version = stream_read_uint32(&ctx->stream); /* version + flags */ + size_remaining -= 4; + + if ((version >> 24) == 0) + { + /* Version 0: creation(4)+modification(4)+track_id(4)+reserved(4)+ + duration(4) = 20 bytes, then reserved(8)+layer(2)+alt_group(2)+ + volume(2)+reserved(2)+matrix(36)+width(4)+height(4) = 56 bytes */ + stream_skip(&ctx->stream, 20 + 8 + 2 + 2 + 2 + 2 + 36); + size_remaining -= 20 + 8 + 2 + 2 + 2 + 2 + 36; + } + else if ((version >> 24) == 1) + { + if (chunk_len < 104) + return false; + /* Version 1: creation(8)+modification(8)+track_id(4)+reserved(4)+ + duration(8) = 32 bytes */ + stream_skip(&ctx->stream, 32 + 8 + 2 + 2 + 2 + 2 + 36); + size_remaining -= 32 + 8 + 2 + 2 + 2 + 2 + 36; + } + else + { + return false; + } + + dw_fp = stream_read_uint32(&ctx->stream); /* width in 16.16 */ + dh_fp = stream_read_uint32(&ctx->stream); /* height in 16.16 */ + size_remaining -= 8; + + /* Commit to res only after mdia confirms video */ + ctx->tkhd_dw = (uint16_t)(dw_fp >> 16); + ctx->tkhd_dh = (uint16_t)(dh_fp >> 16); + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* edts/elst - edit list (extract media_time for encoder priming offset) */ +static bool read_chunk_elst(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining; + uint32_t version_flags; + uint32_t entry_count, i; + + if (chunk_len < 16) + return false; + size_remaining = chunk_len - 8; + version_flags = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + entry_count = stream_read_uint32(&ctx->stream); + size_remaining -= 4; + + { + size_t entry_size = ((version_flags >> 24) == 0) ? 12 : 20; + + if ((version_flags >> 24) > 1 || + entry_count > size_remaining / entry_size) + return false; + for (i = 0; i < entry_count; i++) + { + int32_t media_time; + + if ((version_flags >> 24) == 0) + { + stream_skip(&ctx->stream, 4); /* segment_duration */ + media_time = (int32_t)stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* media_rate (16.16) */ + size_remaining -= 12; + } + else + { + stream_skip(&ctx->stream, 8); /* segment_duration (64-bit) */ + stream_skip(&ctx->stream, 4); /* media_time high */ + media_time = (int32_t)stream_read_uint32(&ctx->stream); + stream_skip(&ctx->stream, 4); /* media_rate */ + size_remaining -= 20; + } + + /* First non-empty edit: media_time > 0 = priming offset. + * Save until the track type is known. */ + if (i == 0 && media_time > 0) + ctx->pending_lead_trim = (uint32_t)media_time; + } + } + + if (size_remaining > 0) + stream_skip(&ctx->stream, size_remaining); + + return true; +} + +/* edts container: walk for elst */ +static bool read_chunk_edts(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + if (sub_id == MAKEFOURCC('e','l','s','t')) + { + if (!read_chunk_elst(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* trak - track container */ +static bool read_chunk_trak(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + /* Reset per-track state (pending fields MUST be reset to prevent + * stale values from one trak leaking into the next) */ + ctx->in_video_trak = false; + ctx->in_audio_trak = false; + ctx->pending_timescale = 0; + ctx->pending_lead_trim = 0; + ctx->tkhd_dw = 0; + ctx->tkhd_dh = 0; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('t','k','h','d'): + if (!read_chunk_tkhd(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('m','d','i','a'): + if (!read_chunk_mdia(ctx, sub_len)) + return false; + break; + case MAKEFOURCC('e','d','t','s'): + /* edts comes before mdia, so the track type is unknown. + * Parse unconditionally, save to pending_lead_trim. + * Committed to audio_lead_trim at end of trak. */ + if (!read_chunk_edts(ctx, sub_len)) + return false; + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + /* Commit pending per-trak fields now that track type is known. + * mdhd timescale and edts/elst are parsed before hdlr identifies + * the track type, so they were saved to pending fields above. */ + if (ctx->in_video_trak && ctx->res->format != 0) + { + ctx->found_video = true; + ctx->res->display_width = ctx->tkhd_dw; + ctx->res->display_height = ctx->tkhd_dh; + ctx->res->timescale = ctx->pending_timescale; + } + + if (ctx->in_audio_trak && ctx->res->audio_format != 0) + { + ctx->found_audio = true; + ctx->res->audio_lead_trim = ctx->pending_lead_trim; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +/* moov - movie container */ +static bool read_chunk_moov(struct mp4_parse_ctx *ctx, size_t chunk_len) +{ + size_t size_remaining = chunk_len - 8; + + while (size_remaining >= 8) + { + uint32_t sub_len = stream_read_uint32(&ctx->stream); + uint32_t sub_id = stream_read_uint32(&ctx->stream); + + if (sub_len < 8 || sub_len > size_remaining) + return false; + + switch (sub_id) + { + case MAKEFOURCC('t','r','a','k'): + /* Parse until we have both video and audio (or all traks) */ + if (!ctx->found_video || !ctx->found_audio) + { + if (!read_chunk_trak(ctx, sub_len)) + return false; + } + else + { + stream_skip(&ctx->stream, sub_len - 8); + } + break; + default: + stream_skip(&ctx->stream, sub_len - 8); + break; + } + + size_remaining -= sub_len; + } + + return !ctx->stream.eof && size_remaining == 0; +} + +int mp4v_demux_open(const char *filepath, + struct mp4v_demux_res *res, + uint32_t *sample_buf, uint32_t sample_cap, + uint32_t *chunk_buf, uint32_t chunk_cap, + struct mp4v_stsc_entry *stsc_buf, uint32_t stsc_cap, + uint32_t *sync_buf, uint32_t sync_cap) +{ + struct mp4_parse_ctx ctx; + off_t file_size; + int fd; + + if (filepath == NULL || res == NULL) + return -1; + fd = rb->open(filepath, O_RDONLY); + if (fd < 0) + return -1; + file_size = rb->filesize(fd); + if (file_size <= 0 || (uint64_t)file_size > UINT32_MAX) + { + rb->close(fd); + return -1; + } + + rb->memset(res, 0, sizeof(*res)); + res->sample_sizes = sample_buf; + res->sample_sizes_cap = sample_cap; + res->chunk_offsets = chunk_buf; + res->chunk_offsets_cap = chunk_cap; + res->stsc = stsc_buf; + res->stsc_cap = stsc_cap; + res->sync_samples = sync_buf; + res->sync_samples_cap = sync_cap; + res->file_size = (uint32_t)file_size; + + rb->memset(&ctx, 0, sizeof(ctx)); + stream_init(&ctx.stream, fd, file_size); + ctx.res = res; + + /* Walk top-level boxes */ + while (ctx.stream.pos < file_size && !ctx.stream.eof) + { + uint32_t chunk_len = stream_read_uint32(&ctx.stream); + uint32_t chunk_id; + + if (ctx.stream.eof) + break; + + if (chunk_len < 8) + { + rb->close(fd); + return -1; + } + + chunk_id = stream_read_uint32(&ctx.stream); + if (ctx.stream.eof || + chunk_len - 8 > (uint32_t)(file_size - ctx.stream.pos)) + { + rb->close(fd); + return -1; + } + + switch (chunk_id) + { + case MAKEFOURCC('f','t','y','p'): + stream_skip(&ctx.stream, chunk_len - 8); + break; + case MAKEFOURCC('m','o','o','v'): + if (!read_chunk_moov(&ctx, chunk_len)) + { + rb->close(fd); + return -1; + } + break; + case MAKEFOURCC('m','d','a','t'): + if (chunk_len > 8) + { + res->mdat_offset = (uint32_t)stream_tell(&ctx.stream); + res->mdat_len = chunk_len - 8; + } + /* If we already found everything we need, we're done */ + if (ctx.found_video && ctx.found_audio) + goto done; + stream_skip(&ctx.stream, chunk_len - 8); + break; + default: + stream_skip(&ctx.stream, chunk_len - 8); + break; + } + } + +done: + rb->close(fd); + + if (ctx.stream.eof || !ctx.found_video || res->format == 0) + return -1; + + return 0; +} + +int mp4v_get_sample_offset(const struct mp4v_demux_res *res, + uint32_t sample_index, + uint32_t *offset_out, + uint32_t *size_out) +{ + uint32_t chunk_index = 0; + uint32_t sample_in_chunk = 0; + uint64_t samples_so_far = 0; + uint64_t chunk_offset; + uint32_t i; + bool found = false; + + if (res == NULL || offset_out == NULL || size_out == NULL || + res->sample_sizes == NULL || res->chunk_offsets == NULL || + res->stsc == NULL || sample_index >= res->num_samples || + sample_index >= res->sample_sizes_cap || + res->num_stsc > res->stsc_cap || + res->num_stco > res->chunk_offsets_cap) + return -1; + + *size_out = res->sample_sizes[sample_index]; + + /* Walk stsc to find which chunk contains this sample */ + for (i = 0; i < res->num_stsc; i++) + { + uint32_t first_chunk; + uint32_t spc = res->stsc[i].samples_per_chunk; + uint32_t next_first; + + if (res->stsc[i].first_chunk == 0 || spc == 0) + return -1; + first_chunk = res->stsc[i].first_chunk - 1; + if (i + 1 < res->num_stsc) + { + if (res->stsc[i + 1].first_chunk == 0) + return -1; + next_first = res->stsc[i + 1].first_chunk - 1; + } + else + next_first = res->num_stco; + + if (next_first <= first_chunk || next_first > res->num_stco) + return -1; + uint32_t chunks_in_run = next_first - first_chunk; + uint64_t samples_in_run = (uint64_t)chunks_in_run * spc; + + if (samples_so_far + samples_in_run > sample_index) + { + /* Sample is within this stsc run */ + uint32_t sample_offset_in_run = sample_index - samples_so_far; + chunk_index = first_chunk + (sample_offset_in_run / spc); + sample_in_chunk = sample_offset_in_run % spc; + found = true; + break; + } + + samples_so_far += samples_in_run; + } + + if (!found || chunk_index >= res->num_stco) + return -1; + + /* Get the chunk's file offset */ + chunk_offset = res->chunk_offsets[chunk_index]; + + /* Add offsets for preceding samples within this chunk */ + { + uint32_t first_sample_in_chunk = sample_index - sample_in_chunk; + uint32_t j; + + for (j = 0; j < sample_in_chunk; j++) + { + uint32_t index = first_sample_in_chunk + j; + + if (index >= res->num_samples || index >= res->sample_sizes_cap) + return -1; + chunk_offset += res->sample_sizes[index]; + } + } + + if (chunk_offset > UINT32_MAX || + chunk_offset + *size_out > res->file_size) + return -1; + *offset_out = (uint32_t)chunk_offset; + return 0; +} + +bool mp4v_is_keyframe(const struct mp4v_demux_res *res, + uint32_t sample_index) +{ + uint32_t i; + uint32_t sample_num = sample_index + 1; /* stss is 1-based */ + + /* No stss table means all samples are sync samples */ + if (res->num_stss == 0) + return true; + if (res->sync_samples == NULL || + res->sync_samples_cap < res->num_stss) + return false; + + for (i = 0; i < res->num_stss; i++) + { + if (res->sync_samples[i] == sample_num) + return true; + if (res->sync_samples[i] > sample_num) + break; + } + + return false; +} diff --git a/apps/plugins/h264_player/mp4_demux.h b/apps/plugins/h264_player/mp4_demux.h new file mode 100644 index 0000000000..33d2e02a4d --- /dev/null +++ b/apps/plugins/h264_player/mp4_demux.h @@ -0,0 +1,150 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * H.264 player MP4 demuxer - extracts video track data from MP4/M4V files. + * Forked from libm4a/demux.c, adapted for fd-based I/O and video tracks. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef MP4_DEMUX_H +#define MP4_DEMUX_H + +#include +#include +#include + +/* Max SPS/PPS codec data from avcC box */ +#define MP4V_MAX_CODECDATA 512 + +#define MP4V_MAX_STTS 4096 + +#ifndef MAKEFOURCC +#define MAKEFOURCC(ch0, ch1, ch2, ch3) ( \ + ( (int32_t)(char)(ch0) << 24 ) | \ + ( (int32_t)(char)(ch1) << 16 ) | \ + ( (int32_t)(char)(ch2) << 8 ) | \ + ( (int32_t)(char)(ch3) ) ) +#endif + +struct mp4v_stts_entry { + uint32_t sample_count; + uint32_t sample_delta; +}; + +struct mp4v_stsc_entry { + uint32_t first_chunk; + uint32_t samples_per_chunk; + uint32_t sample_desc_index; +}; + +struct mp4v_demux_res { + /* Video format */ + uint32_t format; /* fourcc: 'avc1' etc. */ + uint16_t width; /* coded width from avc1 sample entry */ + uint16_t height; /* coded height from avc1 sample entry */ + uint16_t display_width; /* display width from tkhd (PAR-adjusted) */ + uint16_t display_height; /* display height from tkhd */ + uint32_t timescale; /* from mdhd */ + + /* avcC decoder config (SPS + PPS) */ + uint8_t avc_profile; + uint8_t avc_level; + uint8_t nalu_len_size; /* length field size (1-4 bytes, typically 4) */ + uint8_t num_sps; + uint8_t num_pps; + uint32_t codecdata_len; + uint8_t codecdata[MP4V_MAX_CODECDATA]; /* raw avcC data */ + + /* Sample sizes (stsz) */ + uint32_t num_samples; + uint32_t *sample_sizes; /* caller-provided buffer */ + uint32_t sample_sizes_cap; + + /* Time-to-sample (stts) */ + uint32_t num_stts; + struct mp4v_stts_entry stts[MP4V_MAX_STTS]; + + /* Sample-to-chunk (stsc) */ + uint32_t num_stsc; + struct mp4v_stsc_entry *stsc; /* caller-provided buffer */ + uint32_t stsc_cap; + + /* Chunk offsets (stco) */ + uint32_t num_stco; + uint32_t *chunk_offsets; /* caller-provided buffer */ + uint32_t chunk_offsets_cap; + + /* Sync samples / keyframes (stss) */ + uint32_t num_stss; + uint32_t *sync_samples; /* caller-provided buffer */ + uint32_t sync_samples_cap; + + /* File and mdat location */ + uint32_t file_size; + uint32_t mdat_offset; + uint32_t mdat_len; + + /* Audio track (populated when mp4a trak found) */ + uint32_t audio_format; /* fourcc: 'mp4a' or 0 if no audio */ + uint32_t audio_sample_rate; /* from mp4a sample entry */ + uint8_t audio_codecdata[64]; /* AudioSpecificConfig from esds */ + uint32_t audio_codecdata_len; + uint32_t audio_lead_trim; /* encoder priming from edts/elst */ + + /* Audio table counts used to size aac.codec's workspace. The AAC codec + * parses its own table data from the file. */ + uint32_t audio_num_samples; + uint32_t audio_num_stsc; + uint32_t audio_num_stco; +}; + +/* Open and parse an MP4 file, extracting video and audio tracks. + * + * filepath: path to the .mp4/.m4v file + * res: output struct (caller allocates) + * sample_buf: buffer for video sample_sizes array + * sample_cap: number of uint32_t entries in sample_buf + * chunk_buf: buffer for video chunk_offsets array + * chunk_cap: number of uint32_t entries in chunk_buf + * stsc_buf: buffer for video sample-to-chunk entries + * stsc_cap: number of entries in stsc_buf + * sync_buf: buffer for video sync-sample entries + * sync_cap: number of entries in sync_buf + * + * Returns 0 on success (video found), -1 on error. + * Audio track is optional; check res->audio_format != 0. */ +int mp4v_demux_open(const char *filepath, + struct mp4v_demux_res *res, + uint32_t *sample_buf, uint32_t sample_cap, + uint32_t *chunk_buf, uint32_t chunk_cap, + struct mp4v_stsc_entry *stsc_buf, uint32_t stsc_cap, + uint32_t *sync_buf, uint32_t sync_cap); + +/* Get the file offset and size of video sample N (0-based). + * Returns 0 on success, -1 on error. */ +int mp4v_get_sample_offset(const struct mp4v_demux_res *res, + uint32_t sample_index, + uint32_t *offset_out, + uint32_t *size_out); + +/* Check if video sample N is a keyframe (sync sample). + * Returns true if it's a keyframe or if no stss table exists + * (meaning all samples are sync samples). */ +bool mp4v_is_keyframe(const struct mp4v_demux_res *res, + uint32_t sample_index); + +#endif /* MP4_DEMUX_H */ diff --git a/apps/plugins/h264_player/video_audio.c b/apps/plugins/h264_player/video_audio.c new file mode 100644 index 0000000000..2f8434377e --- /dev/null +++ b/apps/plugins/h264_player/video_audio.c @@ -0,0 +1,563 @@ +/*************************************************************************** + * AAC audio service for the native S5L8702 H.264 player. + * + * The decoder itself remains Rockbox's normal dynamically-loaded aac.codec. + * That is important on iPod 6G: FAAD's hot code and working state are an IRAM + * overlay and cannot be linked into the permanent core without overflowing + * IRAM. This file borrows the existing codec thread, supplies file-buffer + * callbacks for the MP4, and sends decoded PCM to video_pcm. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include + +#include "codec_thread.h" +#include "codecs.h" +#include "file.h" +#include "kernel.h" +#include "metadata.h" +#include "mp4_demux.h" +#include "video_audio.h" +#include "video_pcm.h" + +#include +#include + +#define AUDIO_FILE_BUFFER_SIZE 8192 +#define AUDIO_PCM_FRAMES 1024 +#define AUDIO_START_BUFFER_MS 1000 +/* libm4a's sample_offset_t stores a uint32_t sample and file offset. Keep a + * normal CODEC_SIZE of headroom after the complete Apple chunk map. */ +#define AUDIO_CHUNK_LOOKUP_ENTRY_SIZE (2u * sizeof(uint32_t)) + +static struct codec_api video_ci; +static struct mp3entry video_id3; +static int audio_fd = -1; +static off_t audio_file_size; +static uint8_t audio_file_buffer[AUDIO_FILE_BUFFER_SIZE]; +static off_t audio_file_buffer_offset; +static size_t audio_file_buffer_length; +static int16_t audio_pcm[AUDIO_PCM_FRAMES * 2]; +static void *audio_codec_workspace; +static size_t audio_codec_workspace_size; +static uint32_t audio_sample_rate; +static int audio_sample_depth; +static int audio_stereo_mode; +static volatile long audio_action; +static volatile uint32_t audio_seek_target_ms; +static volatile bool audio_paused; +static volatile bool audio_is_ready; +static volatile bool audio_is_playing; +static volatile bool audio_has_error; +static bool audio_initialized; + +size_t video_audio_workspace_size(const struct mp4v_demux_res *demux) +{ + size_t table_size; + + if (demux == NULL || demux->audio_num_samples == 0 || + demux->audio_num_stco == 0 || + demux->audio_num_stco > + (SIZE_MAX - CODEC_SIZE) / AUDIO_CHUNK_LOOKUP_ENTRY_SIZE) + return 0; + table_size = (size_t)demux->audio_num_stco * + AUDIO_CHUNK_LOOKUP_ENTRY_SIZE; + return CODEC_SIZE + table_size; +} + +static int16_t audio_clip16(int32_t value) +{ + if (value > 32767) + return 32767; + if (value < -32768) + return -32768; + return (int16_t)value; +} + +static void *audio_codec_get_buffer(size_t *size) +{ + *size = audio_codec_workspace_size; + return audio_codec_workspace; +} + +static bool audio_refill(off_t position) +{ + ssize_t count; + + if (position < 0 || position > audio_file_size || + rb->lseek(audio_fd, position, SEEK_SET) < 0) + return false; + count = rb->read(audio_fd, audio_file_buffer, sizeof(audio_file_buffer)); + if (count < 0) + return false; + audio_file_buffer_offset = position; + audio_file_buffer_length = (size_t)count; + return true; +} + +static size_t audio_read_filebuf(void *destination, size_t size) +{ + size_t done = 0; + + while (done < size && video_ci.curpos < audio_file_size) + { + size_t in_buffer; + size_t chunk; + + if (video_ci.curpos < audio_file_buffer_offset || + video_ci.curpos >= audio_file_buffer_offset + + (off_t)audio_file_buffer_length) + { + if (!audio_refill(video_ci.curpos)) + break; + } + in_buffer = (size_t)(video_ci.curpos - audio_file_buffer_offset); + chunk = audio_file_buffer_length - in_buffer; + if (chunk > size - done) + chunk = size - done; + if (chunk == 0) + break; + rb->memcpy((uint8_t *)destination + done, + audio_file_buffer + in_buffer, chunk); + video_ci.curpos += chunk; + done += chunk; + } + return done; +} + +static void *audio_request_buffer(size_t *real_size, size_t requested) +{ + size_t available; + size_t in_buffer; + size_t needed; + off_t remaining; + + if (video_ci.curpos >= audio_file_size) + { + *real_size = 0; + return audio_file_buffer; + } + + remaining = audio_file_size - video_ci.curpos; + needed = requested; + if ((off_t)needed > remaining) + needed = (size_t)remaining; + + if (video_ci.curpos < audio_file_buffer_offset || + video_ci.curpos >= audio_file_buffer_offset + + (off_t)audio_file_buffer_length) + { + if (!audio_refill(video_ci.curpos)) + { + *real_size = 0; + return audio_file_buffer; + } + } + in_buffer = (size_t)(video_ci.curpos - audio_file_buffer_offset); + available = audio_file_buffer_length - in_buffer; + + /* codec_api.request_buffer promises the requested span whenever that + * many file bytes remain. Do not return the short tail of our 8 KiB + * cache: FAAD treats a truncated AAC frame as a fatal decode error. */ + if (available < needed) + { + if (!audio_refill(video_ci.curpos)) + { + *real_size = 0; + return audio_file_buffer; + } + in_buffer = 0; + available = audio_file_buffer_length; + } + + if (available > needed) + available = needed; + *real_size = available; + return audio_file_buffer + in_buffer; +} + +static void audio_advance_buffer(size_t amount) +{ + off_t remaining = audio_file_size - video_ci.curpos; + + if ((off_t)amount > remaining) + amount = remaining > 0 ? (size_t)remaining : 0; + video_ci.curpos += amount; + video_id3.offset = video_ci.curpos; +} + +static bool audio_seek_buffer(size_t position) +{ + if ((off_t)position > audio_file_size) + return false; + video_ci.curpos = position; + video_id3.offset = position; + return true; +} + +static void audio_seek_complete(void) +{ +} + +static void audio_set_elapsed(unsigned long elapsed) +{ + video_id3.elapsed = elapsed; +} + +static void audio_set_offset(size_t offset) +{ + video_id3.offset = offset; +} + +static void audio_configure(int setting, intptr_t value) +{ + switch (setting) + { + case DSP_SET_FREQUENCY: + if (value > 0) + audio_sample_rate = (uint32_t)value; + break; + case DSP_SET_SAMPLE_DEPTH: + audio_sample_depth = (int)value; + break; + case DSP_SET_STEREO_MODE: + audio_stereo_mode = (int)value; + break; + default: + break; + } +} + +static long audio_get_command(intptr_t *parameter) +{ + long action; + + while (audio_paused && audio_action == CODEC_ACTION_NULL) + rb->sleep(1); + rb->yield(); + action = audio_action; + if (action == CODEC_ACTION_SEEK_TIME) + { + if (parameter != NULL) + *parameter = (intptr_t)audio_seek_target_ms; + /* A seek is a one-shot command. HALT deliberately remains latched + * until codec teardown so the producer cannot resume behind us. */ + audio_action = CODEC_ACTION_NULL; + } + return action; +} + +static bool audio_loop_track(void) +{ + return false; +} + +static void audio_strip_filesize(off_t size) +{ + video_ci.filesize = size; +} + +static int32_t audio_sample_value(const void *channel, int index) +{ + if (audio_sample_depth <= 16) + return ((const int16_t *)channel)[index]; + + { + int shift = audio_sample_depth + 1 - 16; + int32_t value = ((const int32_t *)channel)[index]; + int32_t bias = shift > 0 ? (int32_t)1 << (shift - 1) : 0; + + return shift > 0 ? (value + bias) >> shift : value; + } +} + +static void audio_pcm_insert(const void *channel1, const void *channel2, + int count) +{ + int consumed = 0; + + while (consumed < count && audio_action != CODEC_ACTION_HALT) + { + int frames; + int written = 0; + int i; + + while (audio_paused && audio_action != CODEC_ACTION_HALT) + rb->sleep(1); + if (audio_action == CODEC_ACTION_HALT) + break; + + frames = count - consumed; + if (frames > AUDIO_PCM_FRAMES) + frames = AUDIO_PCM_FRAMES; + for (i = 0; i < frames; i++) + { + int source = consumed + i; + int32_t left; + int32_t right; + + if (audio_stereo_mode == STEREO_INTERLEAVED) + { + left = audio_sample_value(channel1, source * 2); + right = audio_sample_value(channel1, source * 2 + 1); + } + else + { + left = audio_sample_value(channel1, source); + right = audio_stereo_mode == STEREO_MONO ? left : + audio_sample_value(channel2, source); + } + audio_pcm[i * 2] = audio_clip16(left); + audio_pcm[i * 2 + 1] = audio_clip16(right); + } + while (written < frames && audio_action != CODEC_ACTION_HALT) + { + int amount; + + while (audio_paused && audio_action != CODEC_ACTION_HALT) + rb->sleep(1); + amount = video_pcm_write(audio_pcm + written * 2, + frames - written); + if (amount == 0) + { + rb->sleep(1); + continue; + } + written += amount; + } + consumed += frames; + if (!audio_is_ready && audio_sample_rate > 0 && + video_pcm_buffered_samples() >= + audio_sample_rate * AUDIO_START_BUFFER_MS / 1000) + audio_is_ready = true; + } +} + +static void audio_codec_api_init(void) +{ + rb->memset(&video_ci, 0, sizeof(video_ci)); + + video_ci.dsp = rb->dsp_get_config(CODEC_IDX_AUDIO); + video_ci.codec_get_buffer = audio_codec_get_buffer; + video_ci.pcmbuf_insert = audio_pcm_insert; + video_ci.set_elapsed = audio_set_elapsed; + video_ci.read_filebuf = audio_read_filebuf; + video_ci.request_buffer = audio_request_buffer; + video_ci.advance_buffer = audio_advance_buffer; + video_ci.seek_buffer = audio_seek_buffer; + video_ci.seek_complete = audio_seek_complete; + video_ci.set_offset = audio_set_offset; + video_ci.configure = audio_configure; + video_ci.get_command = audio_get_command; + video_ci.loop_track = audio_loop_track; + video_ci.strip_filesize = audio_strip_filesize; + +#if defined(ARM_NEED_DIV0) + video_ci.__div0 = rb->__div0; +#endif + video_ci.sleep = rb->sleep; + video_ci.yield = rb->yield; + +#if NUM_CORES > 1 + video_ci.create_thread = rb->create_thread; + video_ci.thread_thaw = rb->thread_thaw; + video_ci.thread_wait = rb->thread_wait; + video_ci.semaphore_init = rb->semaphore_init; + video_ci.semaphore_wait = rb->semaphore_wait; + video_ci.semaphore_release = rb->semaphore_release; +#endif + + video_ci.commit_dcache = rb->commit_dcache; + video_ci.commit_discard_dcache = rb->commit_discard_dcache; + video_ci.commit_discard_idcache = rb->commit_discard_idcache; + video_ci.strcpy = rb->strcpy; + video_ci.strlen = rb->strlen; + video_ci.strcmp = rb->strcmp; + video_ci.strcat = rb->strcat; + video_ci.memset = rb->memset; + video_ci.memcpy = rb->memcpy; + video_ci.memmove = rb->memmove; + video_ci.memcmp = rb->memcmp; + video_ci.memchr = rb->memchr; +#if defined(DEBUG) || defined(SIMULATOR) + video_ci.debugf = rb->debugf; +#endif +#ifdef ROCKBOX_HAS_LOGF + video_ci.logf = rb->logf; +#endif + video_ci.qsort = rb->qsort; + +#ifdef RB_PROFILE + video_ci.profile_thread = rb->profile_thread; + video_ci.profstop = rb->profstop; + video_ci.profile_func_enter = rb->profile_func_enter; + video_ci.profile_func_exit = rb->profile_func_exit; +#endif + +#ifdef HAVE_RECORDING + video_ci.round_value_to_list32 = rb->round_value_to_list32; +#endif + video_ci.panicf = rb->panicf; +} + +static void audio_codec_worker(void) +{ + int status; +#ifdef HAVE_PRIORITY_SCHEDULING + /* Match mpegplayer: core DSP/AAC yields often enough that the normal + * codec priority can be starved by the video render loop. */ + int old_priority = rb->thread_set_priority(rb->thread_self(), + PRIORITY_PLAYBACK - 4); +#endif + + status = rb->codec_load_file("aac", &video_ci); + if (status >= 0) + status = rb->codec_run_proc(); + rb->codec_close(); +#ifdef HAVE_PRIORITY_SCHEDULING + rb->thread_set_priority(rb->thread_self(), old_priority); +#endif + if (status < 0 && audio_action != CODEC_ACTION_HALT) + audio_has_error = true; + audio_is_ready = true; + audio_is_playing = false; +} + +int video_audio_init(const char *filepath, + const struct mp4v_demux_res *demux, + void *codec_workspace, size_t workspace_size, + void *pcm_buffer, size_t pcm_buffer_size) +{ + size_t required_workspace = video_audio_workspace_size(demux); + + if (audio_initialized || filepath == NULL || demux == NULL || + codec_workspace == NULL || pcm_buffer == NULL || + required_workspace == 0 || + workspace_size < required_workspace || + demux->audio_format != MAKEFOURCC('m', 'p', '4', 'a') || + demux->audio_sample_rate == 0 || demux->audio_num_samples == 0) + return -1; + + audio_fd = rb->open(filepath, O_RDONLY); + if (audio_fd < 0) + return -1; + audio_file_size = rb->filesize(audio_fd); + if (audio_file_size <= 0) + { + rb->close(audio_fd); + audio_fd = -1; + return -1; + } + + audio_codec_workspace = codec_workspace; + audio_codec_workspace_size = workspace_size; + audio_file_buffer_offset = 0; + audio_file_buffer_length = 0; + audio_sample_rate = demux->audio_sample_rate; + audio_sample_depth = 29; + audio_stereo_mode = STEREO_NONINTERLEAVED; + audio_action = CODEC_ACTION_NULL; + audio_seek_target_ms = 0; + audio_paused = false; + audio_is_ready = false; + audio_is_playing = false; + audio_has_error = false; + + rb->memset(&video_id3, 0, sizeof(video_id3)); + video_id3.codectype = AFMT_MP4_AAC; + video_id3.frequency = demux->audio_sample_rate; + video_id3.lead_trim = demux->audio_lead_trim; + + audio_codec_api_init(); + video_ci.filesize = audio_file_size; + video_ci.curpos = 0; + video_ci.id3 = &video_id3; + video_ci.audio_hid = -1; + if (!video_pcm_init(audio_sample_rate, pcm_buffer, pcm_buffer_size)) + { + rb->close(audio_fd); + audio_fd = -1; + audio_codec_workspace = NULL; + audio_codec_workspace_size = 0; + return -1; + } + audio_initialized = true; + return 0; +} + +void video_audio_play(void) +{ + if (!audio_initialized || audio_is_playing) + return; + audio_action = CODEC_ACTION_NULL; + audio_paused = false; + audio_is_playing = true; + rb->codec_thread_do_callback(audio_codec_worker, NULL); +} + +void video_audio_pause(void) +{ + audio_paused = true; +} + +void video_audio_resume(void) +{ + audio_paused = false; +} + +void video_audio_seek(uint32_t target_ms) +{ + if (!audio_initialized || !audio_is_playing) + return; + + audio_is_ready = false; + audio_seek_target_ms = target_ms; + video_pcm_flush(target_ms, audio_sample_rate); + audio_action = CODEC_ACTION_SEEK_TIME; +} + +void video_audio_stop(void) +{ + if (!audio_initialized) + return; + + audio_action = CODEC_ACTION_HALT; + audio_paused = false; + rb->codec_thread_do_callback(NULL, NULL); + video_pcm_stop(); + if (audio_fd >= 0) + { + rb->close(audio_fd); + audio_fd = -1; + } + audio_codec_workspace = NULL; + audio_codec_workspace_size = 0; + audio_initialized = false; + audio_is_playing = false; +} + +bool video_audio_ready(void) +{ + return audio_is_ready || audio_has_error; +} + +bool video_audio_is_active(void) +{ + return audio_is_playing; +} + +bool video_audio_failed(void) +{ + return audio_has_error; +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_audio.h b/apps/plugins/h264_player/video_audio.h new file mode 100644 index 0000000000..69c229804a --- /dev/null +++ b/apps/plugins/h264_player/video_audio.h @@ -0,0 +1,60 @@ +/*************************************************************************** + * H.264 player audio decode service. + * + * Decodes AAC audio via Rockbox's aac.codec and feeds a PCM ring buffer. + * Uses the audio clock from video_pcm for A/V synchronization. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_AUDIO_H +#define VIDEO_AUDIO_H + +#include +#include +#include +#include "mp4_demux.h" + +/* Return enough codec heap to retain every AAC chunk offset. Apple M4V files + * interleave audio and video densely, so a sparse chunk map cannot decode the + * audio stream correctly. Returns 0 if the size cannot be represented. */ +size_t video_audio_workspace_size(const struct mp4v_demux_res *demux); + +/* Initialize audio decoder and thread. + * filepath: MP4 file path (opens its own fd for independent seeks) + * demux: parsed MP4 with audio track data (audio_format != 0) + * Returns 0 on success, -1 on error. */ +int video_audio_init(const char *filepath, + const struct mp4v_demux_res *demux, + void *codec_workspace, size_t workspace_size, + void *pcm_buffer, size_t pcm_buffer_size); + +/* Start audio playback (begins decoding and filling PCM buffer). */ +void video_audio_play(void); + +/* Pause audio decode (PCM buffer drains, then silence). */ +void video_audio_pause(void); + +/* Resume audio decode after pause. */ +void video_audio_resume(void); + +/* Seek audio to target time in milliseconds. Flushes PCM buffer. */ +void video_audio_seek(uint32_t target_ms); + +/* Stop audio thread and release resources. Blocks until thread exits. */ +void video_audio_stop(void); + +/* Check if audio thread has pre-filled enough PCM data to start. */ +bool video_audio_ready(void); + +/* Check if audio thread is actively decoding (false after EOF or error). */ +bool video_audio_is_active(void); + +/* True if the dynamically loaded AAC codec failed to load or decode. */ +bool video_audio_failed(void); + +#endif /* VIDEO_AUDIO_H */ diff --git a/apps/plugins/h264_player/video_pcm.c b/apps/plugins/h264_player/video_pcm.c new file mode 100644 index 0000000000..72874bc9c4 --- /dev/null +++ b/apps/plugins/h264_player/video_pcm.c @@ -0,0 +1,250 @@ +/*************************************************************************** + * PCM ring buffer for video player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Single-producer (audio decode thread), single-consumer (DMA ISR). + * Audio-master clock via monotonic sample counter. + * + * get_more() runs in DMA ISR context: no yield, sleep, mutex, or file I/O. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include "system.h" +#include "kernel.h" +#include "pcm.h" +#include "pcm_mixer.h" +#include "pcm_sampr.h" +#include "video_pcm.h" + +#include + +/* Silence clip for underrun (256 stereo samples = ~5.8ms at 44.1kHz) */ +#define SILENCE_SAMPLES 256 + +static int16_t *pcm_buffer; +static uint32_t pcm_buffer_frames; +static int16_t silence[SILENCE_SAMPLES * 2]; + +/* Read/write positions in stereo samples (wrap via modulo) */ +static volatile uint32_t pcm_read_pos; +static volatile uint32_t pcm_write_pos; + +/* Master clock: stereo samples consumed by DMA (uint32_t sufficient + * for >27 hours at 44.1kHz; avoids torn reads on 32-bit ARM) */ +static volatile uint32_t clock_samples; + +/* Clock base: absolute time (ms) at last flush/init */ +static uint32_t clock_base_ms; +static uint32_t clock_sample_rate; + +/* Flush guard: prevents write during flush (set by main, checked by audio) */ +static volatile bool flush_pending; +static struct mutex write_mutex; + +/* Saved mixer frequency for restore on stop */ +static unsigned int saved_sampr; + +static uint32_t pcm_used(void) +{ + return pcm_write_pos - pcm_read_pos; +} + +static uint32_t pcm_free(void) +{ + uint32_t used = pcm_used(); + + return used < pcm_buffer_frames ? pcm_buffer_frames - used : 0; +} + +/* DMA callback runs in ISR context. + * Pulls from ring buffer, advances clock. On underrun, plays silence. */ +static void video_pcm_get_more(const void **start, size_t *size) +{ + uint32_t avail = pcm_used(); + + if (avail > 0) + { + uint32_t rd = pcm_read_pos % pcm_buffer_frames; + uint32_t chunk = avail; + + /* Don't wrap around the buffer end */ + if (rd + chunk > pcm_buffer_frames) + chunk = pcm_buffer_frames - rd; + + /* Limit chunk to one mixer frame so the entire returned buffer + * is consumed before pcm_read_pos advances. Without this, the + * mixer stores our pointer and gradually reads over 8 callbacks + * while the write side sees the positions as "free" and + * overwrites them, causing audible fragment repetition. */ + if (chunk > MIX_FRAME_SAMPLES) + chunk = MIX_FRAME_SAMPLES; + + *start = &pcm_buffer[rd * 2]; + *size = chunk * 4; /* stereo 16-bit = 4 bytes per sample */ + pcm_read_pos += chunk; + clock_samples += chunk; + } + else + { + /* Underrun: play silence. Do NOT advance the clock; + * advancing would create a permanent A/V offset because the + * audio content hasn't actually been played yet. */ + *start = silence; + *size = sizeof(silence); + } +} + +static const struct mixer_play_cbs video_pcm_cbs = { + .get_more = video_pcm_get_more, +}; + +bool video_pcm_init(uint32_t sample_rate, void *buffer, size_t buffer_size) +{ + if (sample_rate == 0 || buffer == NULL || + buffer_size / (2 * sizeof(int16_t)) < MIX_FRAME_SAMPLES) + return false; + + /* Save current frequency for restore */ + saved_sampr = rb->mixer_get_frequency(); + pcm_buffer = buffer; + pcm_buffer_frames = buffer_size / (2 * sizeof(int16_t)); + + /* Reset buffer state */ + pcm_read_pos = 0; + pcm_write_pos = 0; + clock_samples = 0; + clock_base_ms = 0; + clock_sample_rate = sample_rate; + flush_pending = false; + rb->mutex_init(&write_mutex); + rb->memset(silence, 0, sizeof(silence)); + + /* Set sample rate BEFORE starting playback + * (mixer_set_frequency calls mixer_reset which stops all channels) */ +#if INPUT_SRC_CAPS != 0 + rb->audio_set_input_source(AUDIO_SRC_PLAYBACK, SRCF_PLAYBACK); + rb->audio_set_output_source(AUDIO_SRC_PLAYBACK); +#endif + rb->mixer_set_frequency(sample_rate); + + /* Start playback with DMA callback */ + rb->mixer_channel_set_amplitude(PCM_MIXER_CHAN_PLAYBACK, MIX_AMP_UNITY); + rb->mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, + &video_pcm_cbs, NULL, 0); + return true; +} + +int video_pcm_write(const int16_t *pcm, int stereo_samples) +{ + int written = 0; + + rb->mutex_lock(&write_mutex); + if (flush_pending) + { + rb->mutex_unlock(&write_mutex); + return 0; + } + + while (written < stereo_samples) + { + uint32_t space = pcm_free(); + if (space == 0 || flush_pending) + break; + + uint32_t wr = pcm_write_pos % pcm_buffer_frames; + uint32_t chunk = stereo_samples - written; + + if (chunk > space) + chunk = space; + + /* Don't wrap around the buffer end */ + if (wr + chunk > pcm_buffer_frames) + chunk = pcm_buffer_frames - wr; + + rb->memcpy(&pcm_buffer[wr * 2], &pcm[written * 2], chunk * 4); + pcm_write_pos += chunk; + written += chunk; + } + + rb->mutex_unlock(&write_mutex); + return written; +} + +uint32_t video_pcm_get_clock_ms(void) +{ + uint32_t samples = clock_samples; /* atomic 32-bit read on ARM */ + if (clock_sample_rate == 0) + return clock_base_ms; + return clock_base_ms + (uint32_t)((uint64_t)samples * 1000 + / clock_sample_rate); +} + +void video_pcm_flush(uint32_t base_ms, uint32_t sample_rate) +{ + /* Serialize against the codec producer before resetting its indexes. */ + rb->mutex_lock(&write_mutex); + flush_pending = true; + + /* Stop mixer channel to invalidate any stale buffer pointer, + * then reset state, then restart (mpegplayer pattern). */ + rb->mixer_channel_stop(PCM_MIXER_CHAN_PLAYBACK); + + pcm_read_pos = 0; + pcm_write_pos = 0; + clock_samples = 0; + clock_base_ms = base_ms; + clock_sample_rate = sample_rate; + + /* Restart mixer with fresh callback */ + rb->mixer_channel_set_amplitude(PCM_MIXER_CHAN_PLAYBACK, MIX_AMP_UNITY); + rb->mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, + &video_pcm_cbs, NULL, 0); + + flush_pending = false; + rb->mutex_unlock(&write_mutex); +} + +void video_pcm_pause(bool pause) +{ + rb->mixer_channel_play_pause(PCM_MIXER_CHAN_PLAYBACK, !pause); +} + +void video_pcm_stop(void) +{ + flush_pending = false; + + /* Serialize callback removal before plugin-owned memory is released. */ + rb->mixer_channel_stop(PCM_MIXER_CHAN_PLAYBACK); + + /* Restore original sample rate */ + if (saved_sampr != 0) + rb->mixer_set_frequency(saved_sampr); + pcm_buffer = NULL; + pcm_buffer_frames = 0; +} + +bool video_pcm_has_space(int stereo_samples) +{ + return pcm_free() >= (uint32_t)stereo_samples; +} + +bool video_pcm_empty(void) +{ + return pcm_used() == 0; +} + +uint32_t video_pcm_buffered_samples(void) +{ + return pcm_used(); +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_pcm.h b/apps/plugins/h264_player/video_pcm.h new file mode 100644 index 0000000000..964a913597 --- /dev/null +++ b/apps/plugins/h264_player/video_pcm.h @@ -0,0 +1,52 @@ +/*************************************************************************** + * PCM ring buffer for H.264 player audio output. + * + * Simplified version of mpegplayer's pcm_output.c. + * Audio-master clock: DMA callback advances clock_samples monotonically. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_PCM_H +#define VIDEO_PCM_H + +#include +#include +#include + +/* Initialize PCM output at the given sample rate. + * Saves and changes mixer frequency. */ +bool video_pcm_init(uint32_t sample_rate, void *buffer, size_t buffer_size); + +/* Write interleaved stereo int16 PCM to the ring buffer. + * Returns number of stereo samples actually written (may be less if full). */ +int video_pcm_write(const int16_t *pcm, int stereo_samples); + +/* Get audio master clock in milliseconds. + * Returns base_ms + (samples_played * 1000 / sample_rate). */ +uint32_t video_pcm_get_clock_ms(void); + +/* Flush ring buffer and set clock base (for seek). + * After flush, get_clock returns base_ms + samples_played. */ +void video_pcm_flush(uint32_t base_ms, uint32_t sample_rate); + +/* Pause/resume PCM output. */ +void video_pcm_pause(bool pause); + +/* Stop PCM output and restore original mixer frequency. */ +void video_pcm_stop(void); + +/* Check if ring buffer has space for at least n stereo samples. */ +bool video_pcm_has_space(int stereo_samples); + +/* Check if ring buffer is empty (all data played). */ +bool video_pcm_empty(void); + +/* Return number of stereo samples currently buffered. */ +uint32_t video_pcm_buffered_samples(void); + +#endif /* VIDEO_PCM_H */ diff --git a/apps/plugins/h264_player/video_playback.c b/apps/plugins/h264_player/video_playback.c new file mode 100644 index 0000000000..b366079da7 --- /dev/null +++ b/apps/plugins/h264_player/video_playback.c @@ -0,0 +1,1132 @@ +/*************************************************************************** + * S5L8702 hardware H.264 player. + * + * This is deliberately a narrow production contract: non-fragmented MP4/M4V, + * the measured iTunes 9.2.1 Constrained Baseline syntax (Level 1.3 or 3.0, + * one or two advertised reference pictures and ordered slices), + * <= 640x480, <= 30 fps, with optional AAC-LC audio. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#include "plugin.h" + +#ifdef HAVE_HW_H264 + +#include "backlight.h" +#include "button.h" +#include "crc32.h" +#include "dir.h" +#include "file.h" +#include "font.h" +#include "kernel.h" +#include "lang.h" +#include "lcd.h" +#include "menu.h" +#include "misc.h" +#include "mp4_demux.h" +#include "pcmbuf.h" +#include "rbpaths.h" +#include "settings.h" +#include "sound.h" +#include "splash.h" +#include "system.h" +#include "video_audio.h" +#include "video_pcm.h" +#include "video_playback.h" +#include "hw_h264.h" + +#include +#include +#include + +#define VIDEO_MAX_WIDTH 640 +#define VIDEO_MAX_HEIGHT 480 +#define VIDEO_MAX_LEVEL 30 +#define VIDEO_MAX_FPS 30 +#define VIDEO_MAX_SAMPLES 400000u +#define VIDEO_MAX_AUDIO_SAMPLES 600000u +#define VIDEO_READ_BUFFER (512u * 1024u) +#define VIDEO_OUTPUT_BUFFER (LCD_WIDTH * LCD_HEIGHT * 3u / 2u) +#define VIDEO_PCM_BUFFER (256u * 1024u) +#define VIDEO_OSD_SHOW_TICKS (HZ * 4) +#define VIDEO_RESUME_MAGIC 0x48323634u +#define VIDEO_RESUME_FILE PLUGIN_APPS_DATA_DIR \ + "/h264-resume-%08lx.dat" + +enum video_input_action +{ + VIDEO_INPUT_NONE = 0, + VIDEO_INPUT_EXIT, + VIDEO_INPUT_USB, + VIDEO_INPUT_MENU, + VIDEO_INPUT_SEEK_BACK, + VIDEO_INPUT_SEEK_FORWARD, +}; + +struct video_resume_record +{ + uint32_t magic; + uint32_t path_crc; + int32_t frame; + int32_t total_frames; +}; + +struct video_pool +{ + uint8_t *cursor; + uint8_t *end; +}; + +struct video_timing +{ + uint32_t run; + uint32_t in_run; + uint64_t ticks; +}; + +static void *video_pool_take(struct video_pool *pool, size_t size, + size_t alignment) +{ + uintptr_t current; + uintptr_t aligned; + + if (pool == NULL || alignment == 0) + return NULL; + current = (uintptr_t)pool->cursor; + aligned = (current + alignment - 1) & ~(uintptr_t)(alignment - 1); + if (aligned < current || aligned > (uintptr_t)pool->end || + size > (size_t)(pool->end - (uint8_t *)aligned)) + return NULL; + pool->cursor = (uint8_t *)aligned + size; + return (void *)aligned; +} + +static bool video_tables_sane(const struct mp4v_demux_res *demux) +{ + uint64_t timed_samples = 0; + uint32_t i; + bool video_sane = + demux->num_samples > 0 && + demux->num_samples <= VIDEO_MAX_SAMPLES && + demux->num_stco > 0 && + demux->num_stco <= demux->num_samples && + demux->num_stsc > 0 && + demux->num_stsc <= demux->num_stco && + demux->num_stts > 0 && + demux->num_stss <= demux->num_samples && + demux->timescale > 0; + bool audio_sane = + (demux->audio_num_samples == 0 && + demux->audio_num_stco == 0 && demux->audio_num_stsc == 0) || + (demux->audio_num_samples > 0 && + demux->audio_num_samples <= VIDEO_MAX_AUDIO_SAMPLES && + demux->audio_num_stco > 0 && + demux->audio_num_stco <= demux->audio_num_samples && + demux->audio_num_stsc > 0 && + demux->audio_num_stsc <= demux->audio_num_stco); + + if (!video_sane || !audio_sane) + return false; + for (i = 0; i < demux->num_stts; i++) + { + if (demux->stts[i].sample_count == 0 || + demux->stts[i].sample_delta == 0 || + (uint64_t)demux->stts[i].sample_delta * VIDEO_MAX_FPS < + demux->timescale) + return false; + timed_samples += demux->stts[i].sample_count; + if (timed_samples > demux->num_samples) + return false; + } + return timed_samples == demux->num_samples; +} + +/* Validate the actual video mapping after the second, full table pass. The + * old independent stco/stsc caps rejected long files authored by iTunes, + * whose normal layout is one video sample per chunk and may therefore have + * hundreds of thousands of chunks. The MP4 relationships themselves give + * tighter bounds: every chunk contains samples and every stsc run starts at + * a real chunk. */ +static bool video_stsc_sane(const struct mp4v_demux_res *demux) +{ + uint64_t mapped_samples = 0; + uint32_t i; + + if (demux->stsc == NULL || demux->num_stsc == 0 || + demux->stsc_cap < demux->num_stsc || + demux->sample_sizes == NULL || + demux->sample_sizes_cap < demux->num_samples || + demux->chunk_offsets == NULL || + demux->chunk_offsets_cap < demux->num_stco || + (demux->num_stss > 0 && + (demux->sync_samples == NULL || + demux->sync_samples_cap < demux->num_stss))) + return false; + for (i = 0; i < demux->num_stsc; i++) + { + const struct mp4v_stsc_entry *entry = &demux->stsc[i]; + uint32_t next_chunk = i + 1 < demux->num_stsc ? + demux->stsc[i + 1].first_chunk : demux->num_stco + 1; + + if ((i == 0 && entry->first_chunk != 1) || + entry->first_chunk == 0 || entry->first_chunk > demux->num_stco || + next_chunk <= entry->first_chunk || + next_chunk > demux->num_stco + 1 || + entry->samples_per_chunk == 0 || entry->sample_desc_index == 0) + return false; + mapped_samples += + (uint64_t)(next_chunk - entry->first_chunk) * + entry->samples_per_chunk; + if (mapped_samples > demux->num_samples) + return false; + } + if (mapped_samples != demux->num_samples) + return false; + for (i = 0; i < demux->num_stss; i++) + { + if (demux->sync_samples[i] == 0 || + demux->sync_samples[i] > demux->num_samples || + (i > 0 && demux->sync_samples[i] <= + demux->sync_samples[i - 1])) + return false; + } + return true; +} + +static uint32_t video_pts_ms(const struct mp4v_demux_res *demux, + const struct video_timing *timing) +{ + if (demux->timescale == 0) + return 0; + return (uint32_t)(timing->ticks * 1000u / demux->timescale); +} + +static void video_timing_advance(const struct mp4v_demux_res *demux, + struct video_timing *timing) +{ + const struct mp4v_stts_entry *entry; + + if (timing->run >= demux->num_stts) + return; + entry = &demux->stts[timing->run]; + timing->ticks += entry->sample_delta; + timing->in_run++; + if (timing->in_run >= entry->sample_count) + { + timing->run++; + timing->in_run = 0; + } +} + +static uint32_t video_duration_ms(const struct mp4v_demux_res *demux) +{ + uint64_t ticks = 0; + uint32_t i; + + if (demux->timescale == 0) + return 0; + for (i = 0; i < demux->num_stts; i++) + ticks += (uint64_t)demux->stts[i].sample_count * + demux->stts[i].sample_delta; + return (uint32_t)(ticks * 1000u / demux->timescale); +} + +static void video_timing_for_sample(const struct mp4v_demux_res *demux, + uint32_t sample, + struct video_timing *timing) +{ + uint32_t remaining = sample; + + rb->memset(timing, 0, sizeof(*timing)); + while (timing->run < demux->num_stts) + { + const struct mp4v_stts_entry *entry = + &demux->stts[timing->run]; + + if (remaining < entry->sample_count) + { + timing->in_run = remaining; + timing->ticks += (uint64_t)remaining * entry->sample_delta; + return; + } + timing->ticks += (uint64_t)entry->sample_count * + entry->sample_delta; + remaining -= entry->sample_count; + timing->run++; + } +} + +static uint32_t video_sample_for_ms(const struct mp4v_demux_res *demux, + uint32_t target_ms, + struct video_timing *timing) +{ + uint64_t target_ticks; + uint64_t ticks = 0; + uint32_t sample = 0; + uint32_t run; + + if (demux->timescale == 0) + { + rb->memset(timing, 0, sizeof(*timing)); + return 0; + } + target_ticks = (uint64_t)target_ms * demux->timescale / 1000u; + for (run = 0; run < demux->num_stts; run++) + { + const struct mp4v_stts_entry *entry = &demux->stts[run]; + uint64_t run_ticks = (uint64_t)entry->sample_count * + entry->sample_delta; + + if (target_ticks < ticks + run_ticks) + { + uint32_t in_run = entry->sample_delta == 0 ? 0 : + (uint32_t)((target_ticks - ticks) / entry->sample_delta); + + if (in_run >= entry->sample_count) + in_run = entry->sample_count - 1; + sample += in_run; + break; + } + ticks += run_ticks; + sample += entry->sample_count; + } + if (sample >= demux->num_samples) + sample = demux->num_samples - 1; + while (sample > 0 && !mp4v_is_keyframe(demux, sample)) + sample--; + video_timing_for_sample(demux, sample, timing); + return sample; +} + +static void video_resume_filename(const char *path, char *filename, + size_t size, uint32_t *crc_out) +{ + uint32_t crc = rb->crc_32(path, rb->strlen(path), 0xffffffff); + + rb->snprintf(filename, size, VIDEO_RESUME_FILE, (unsigned long)crc); + if (crc_out != NULL) + *crc_out = crc; +} + +static uint32_t video_resume_load(const char *path, uint32_t total_samples) +{ + struct video_resume_record record; + char filename[MAX_PATH]; + uint32_t crc; + int fd; + + video_resume_filename(path, filename, sizeof(filename), &crc); + fd = rb->open(filename, O_RDONLY); + if (fd < 0) + return 0; + if (rb->read(fd, &record, sizeof(record)) != sizeof(record)) + { + rb->close(fd); + return 0; + } + rb->close(fd); + if (record.magic != VIDEO_RESUME_MAGIC || record.path_crc != crc || + record.total_frames != (int32_t)total_samples || record.frame <= 0 || + record.frame >= (int32_t)(total_samples * 95u / 100u)) + return 0; + return (uint32_t)record.frame; +} + +static void video_resume_save(const char *path, uint32_t sample, + uint32_t total_samples) +{ + struct video_resume_record record; + char filename[MAX_PATH]; + uint32_t crc; + int fd; + + video_resume_filename(path, filename, sizeof(filename), &crc); + if (sample == 0 || sample >= total_samples * 95u / 100u) + { + rb->remove(filename); + return; + } + rb->mkdir(PLUGIN_APPS_DATA_DIR); + record.magic = VIDEO_RESUME_MAGIC; + record.path_crc = crc; + record.frame = (int32_t)sample; + record.total_frames = (int32_t)total_samples; + fd = rb->open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + return; + (void)rb->write(fd, &record, sizeof(record)); + rb->close(fd); +} + +static void video_resume_clear(const char *path) +{ + char filename[MAX_PATH]; + + video_resume_filename(path, filename, sizeof(filename), NULL); + rb->remove(filename); +} + +static void video_volume_change(int delta) +{ + int minimum = rb->sound_min(SOUND_VOLUME); + int maximum = rb->sound_max(SOUND_VOLUME); + int volume = rb->global_status->volume + delta; + + if (volume < minimum) + volume = minimum; + if (volume > maximum) + volume = maximum; + if (volume != rb->global_status->volume) + rb->sound_set(SOUND_VOLUME, volume); +} + +static void video_set_paused(bool pause, bool *paused, long *start_tick, + long *pause_started, bool have_audio, + bool *cpu_boosted) +{ + if (pause == *paused) + return; + if (pause) + { + *paused = true; + *pause_started = (*rb->current_tick); + if (have_audio) + { + video_audio_pause(); + video_pcm_pause(true); + } + if (*cpu_boosted) + { + rb->cpu_boost(false); + *cpu_boosted = false; + } + } + else + { + if (!*cpu_boosted) + { + rb->cpu_boost(true); + *cpu_boosted = true; + } + *paused = false; + *start_tick += (*rb->current_tick) - *pause_started; + if (have_audio) + { + video_audio_resume(); + video_pcm_pause(false); + } + } +} + +static enum video_input_action video_input(bool *paused, long *start_tick, + long *pause_started, + bool have_audio, + bool *cpu_boosted, + long *overlay_until, + bool *redraw) +{ + int action = rb->get_action(CONTEXT_WPS, TIMEOUT_NOBLOCK); + + *redraw = false; + if (action == ACTION_NONE) + return VIDEO_INPUT_NONE; + if (action == ACTION_WPS_MENU) + return VIDEO_INPUT_MENU; + if (action == ACTION_WPS_STOP) + return VIDEO_INPUT_EXIT; + if (action == ACTION_WPS_VOLUP) + { + video_volume_change(1); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; + } + if (action == ACTION_WPS_VOLDOWN) + { + video_volume_change(-1); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; + } + if (action == ACTION_WPS_SKIPPREV || action == ACTION_WPS_SEEKBACK) + return VIDEO_INPUT_SEEK_BACK; + if (action == ACTION_WPS_SKIPNEXT || action == ACTION_WPS_SEEKFWD) + return VIDEO_INPUT_SEEK_FORWARD; + if (action == ACTION_WPS_PLAY) + { + video_set_paused(!*paused, paused, start_tick, pause_started, + have_audio, cpu_boosted); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + } + if (rb->default_event_handler(action) == SYS_USB_CONNECTED) + return VIDEO_INPUT_USB; + return VIDEO_INPUT_NONE; +} + +enum video_menu_item +{ + VIDEO_MENU_RESUME = 0, + VIDEO_MENU_QUIT, +}; + +static enum video_input_action video_show_menu( + bool *paused, long *start_tick, long *pause_started, bool have_audio, + bool *cpu_boosted, long *overlay_until, bool *redraw) +{ + bool resume_after = !*paused; + int selection; + + MENUITEM_STRINGLIST(menu, "H.264 Player", NULL, + ID2P(LANG_RESUME_PLAYBACK), + ID2P(LANG_MENU_QUIT)); + + video_set_paused(true, paused, start_tick, pause_started, have_audio, + cpu_boosted); + rb->button_clear_queue(); + selection = rb->do_menu(&menu, NULL, NULL, false); + rb->button_clear_queue(); + if (selection == MENU_ATTACHED_USB) + return VIDEO_INPUT_USB; + if (selection == VIDEO_MENU_QUIT) + return VIDEO_INPUT_EXIT; + if (resume_after) + video_set_paused(false, paused, start_tick, pause_started, + have_audio, cpu_boosted); + *overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + *redraw = true; + return VIDEO_INPUT_NONE; +} + +static void video_scale_plane(uint8_t *destination, int destination_stride, + int destination_width, int destination_height, + const uint8_t *source, int source_stride, + int source_width, int source_height) +{ + uint32_t source_y = 0; + uint32_t step_y; + uint32_t step_x; + int row; + + if (destination_width <= 0 || destination_height <= 0 || + source_width <= 0 || source_height <= 0) + return; + step_x = ((uint32_t)source_width << 16) / destination_width; + step_y = ((uint32_t)source_height << 16) / destination_height; + for (row = 0; row < destination_height; row++) + { + const uint8_t *source_row = source + + (source_y >> 16) * source_stride; + uint8_t *destination_row = destination + + row * destination_stride; + uint32_t source_x = 0; + int column; + + for (column = 0; column < destination_width; column++) + { + destination_row[column] = source_row[source_x >> 16]; + source_x += step_x; + } + source_y += step_y; + } +} + +static uint8_t video_clamp_yuv(int value) +{ + if (value < 0) + return 0; + if (value > 255) + return 255; + return (uint8_t)value; +} + +static void video_yuv_color(int red, int green, int blue, + uint8_t *y, uint8_t *u, uint8_t *v) +{ + *y = video_clamp_yuv( + ((66 * red + 129 * green + 25 * blue + 128) >> 8) + 16); + *u = video_clamp_yuv( + ((-38 * red - 74 * green + 112 * blue + 128) >> 8) + 128); + *v = video_clamp_yuv( + ((112 * red - 94 * green - 18 * blue + 128) >> 8) + 128); +} + +static void video_yuv_pixel(uint8_t * const *planes, int x, int y, + int red, int green, int blue) +{ + uint8_t py; + uint8_t pu; + uint8_t pv; + + if (x < 0 || x >= LCD_WIDTH || y < 0 || y >= LCD_HEIGHT) + return; + video_yuv_color(red, green, blue, &py, &pu, &pv); + planes[0][y * LCD_WIDTH + x] = py; + planes[1][(y / 2) * (LCD_WIDTH / 2) + x / 2] = pu; + planes[2][(y / 2) * (LCD_WIDTH / 2) + x / 2] = pv; +} + +static void video_yuv_rect(uint8_t * const *planes, int x, int y, + int width, int height, + int red, int green, int blue) +{ + int row; + int column; + + for (row = MAX(0, y); row < MIN(LCD_HEIGHT, y + height); row++) + for (column = MAX(0, x); + column < MIN(LCD_WIDTH, x + width); column++) + video_yuv_pixel(planes, column, row, red, green, blue); +} + +static int video_yuv_text_width(const char *text) +{ + struct font *font = rb->font_get(FONT_SYSFIXED); + int width = 0; + + if (font == NULL) + return 0; + while (*text != '\0') + width += rb->font_get_width(font, (unsigned char)*text++); + return width; +} + +static void video_yuv_text(uint8_t * const *planes, int x, int y, + const char *text, + int red, int green, int blue) +{ + struct font *font = rb->font_get(FONT_SYSFIXED); + + if (font == NULL || font->depth != 0) + return; + while (*text != '\0' && x < LCD_WIDTH) + { + unsigned char ch = *text++; + int glyph_width = rb->font_get_width(font, ch); + const unsigned char *bits = rb->font_get_bits(font, ch); + int column; + + for (column = 0; column < glyph_width; column++) + { + const unsigned char *source = bits + column; + int row; + + for (row = 0; row < (int)font->height; row++) + if (source[(row >> 3) * glyph_width] & + (1u << (row & 7))) + video_yuv_pixel(planes, x + column, y + row, + red, green, blue); + } + x += glyph_width; + } +} + +static void video_time_text(uint32_t milliseconds, char *text, size_t size) +{ + uint32_t seconds = milliseconds / 1000u; + + if (seconds >= 3600u) + rb->snprintf(text, size, "%lu:%02lu:%02lu", + (unsigned long)(seconds / 3600u), + (unsigned long)((seconds / 60u) % 60u), + (unsigned long)(seconds % 60u)); + else + rb->snprintf(text, size, "%lu:%02lu", + (unsigned long)(seconds / 60u), + (unsigned long)(seconds % 60u)); +} + +static void video_draw_status_icon(uint8_t * const *planes, int x, int y, + bool paused) +{ + int row; + + if (paused) + { + video_yuv_rect(planes, x, y, 2, 9, 255, 255, 255); + video_yuv_rect(planes, x + 5, y, 2, 9, 255, 255, 255); + return; + } + for (row = 0; row < 9; row++) + { + int width = row <= 4 ? row + 1 : 9 - row; + + video_yuv_rect(planes, x, y + row, width, 1, 255, 255, 255); + } +} + +static void video_draw_overlay(uint8_t * const *planes, bool paused, + uint32_t position_ms, uint32_t duration_ms, + bool visible) +{ + char current[20]; + char duration[20]; + char volume[20]; + char minimum_volume[20]; + const char *unit = rb->sound_unit(SOUND_VOLUME); + int top = LCD_HEIGHT - 34; + int bar_x = 2; + int bar_y = top + 21; + int bar_height = 8; + int volume_right = LCD_WIDTH - 2; + int volume_field_width; + int volume_width; + int space_width; + int bar_width; + int fill_width; + int duration_width; + + if (!visible) + return; + + video_time_text(position_ms, current, sizeof(current)); + video_time_text(duration_ms, duration, sizeof(duration)); + rb->snprintf(volume, sizeof(volume), "%d%s", + rb->sound_val2phys(SOUND_VOLUME, rb->global_status->volume), unit); + rb->snprintf(minimum_volume, sizeof(minimum_volume), "%d%s", + rb->sound_val2phys(SOUND_VOLUME, + rb->sound_min(SOUND_VOLUME)), unit); + volume_field_width = video_yuv_text_width(minimum_volume); + volume_width = video_yuv_text_width(volume); + space_width = video_yuv_text_width(" "); + bar_width = volume_right - volume_field_width - space_width - bar_x; + fill_width = duration_ms == 0 ? 0 : + (int)((uint64_t)MIN(position_ms, duration_ms) * + (bar_width - 2) / duration_ms); + duration_width = video_yuv_text_width(duration); + + /* Match MPEG player's raised purple OSD and element placement. */ + video_yuv_rect(planes, 0, top, LCD_WIDTH, 34, 115, 117, 189); + video_yuv_rect(planes, 0, top, LCD_WIDTH, 1, 220, 221, 239); + video_yuv_rect(planes, 0, top + 1, LCD_WIDTH, 1, 159, 160, 210); + video_yuv_rect(planes, 0, LCD_HEIGHT - 2, LCD_WIDTH, 1, 93, 95, 153); + video_yuv_rect(planes, 0, LCD_HEIGHT - 1, LCD_WIDTH, 1, 57, 58, 94); + video_yuv_text(planes, bar_x, top + 6, current, 255, 255, 255); + video_yuv_text(planes, bar_x + bar_width - duration_width, top + 6, + duration, 255, 255, 255); + video_draw_status_icon(planes, + bar_x + (bar_width - 7) / 2, + top + 5, paused); + video_yuv_rect(planes, bar_x, bar_y, bar_width, bar_height, + 255, 255, 255); + video_yuv_rect(planes, bar_x + 1, bar_y + 1, + bar_width - 2, bar_height - 2, 0, 0, 0); + video_yuv_rect(planes, bar_x + 1, bar_y + 1, + fill_width, bar_height - 2, 255, 255, 255); + video_yuv_text(planes, volume_right - volume_width, bar_y, + volume, 255, 255, 255); +} + +static void video_draw_frame(const uint8_t *y, const uint8_t *cb, + const uint8_t *cr, int width, int height, + int stride, uint8_t *scaled, bool paused, + uint32_t position_ms, uint32_t duration_ms, + bool overlay_visible) +{ + unsigned char *planes[3]; + int draw_width; + int draw_height; + int x; + int y_pos; + + if (y == NULL || cb == NULL || cr == NULL || scaled == NULL || + width <= 0 || height <= 0 || stride < width) + return; + + draw_width = LCD_WIDTH; + draw_height = (int)((int64_t)height * LCD_WIDTH / width); + if (draw_height > LCD_HEIGHT) + { + draw_height = LCD_HEIGHT; + draw_width = (int)((int64_t)width * LCD_HEIGHT / height); + } + draw_width &= ~1; + draw_height &= ~1; + x = ((LCD_WIDTH - draw_width) / 2) & ~1; + y_pos = ((LCD_HEIGHT - draw_height) / 2) & ~1; + + planes[0] = scaled; + planes[1] = scaled + LCD_WIDTH * LCD_HEIGHT; + planes[2] = planes[1] + LCD_WIDTH * LCD_HEIGHT / 4; + rb->memset(planes[0], 16, LCD_WIDTH * LCD_HEIGHT); + rb->memset(planes[1], 128, LCD_WIDTH * LCD_HEIGHT / 4); + rb->memset(planes[2], 128, LCD_WIDTH * LCD_HEIGHT / 4); + video_scale_plane(planes[0] + y_pos * LCD_WIDTH + x, LCD_WIDTH, + draw_width, draw_height, y, stride, width, height); + video_scale_plane(planes[1] + (y_pos / 2) * (LCD_WIDTH / 2) + x / 2, + LCD_WIDTH / 2, draw_width / 2, draw_height / 2, + cb, stride / 2, width / 2, height / 2); + video_scale_plane(planes[2] + (y_pos / 2) * (LCD_WIDTH / 2) + x / 2, + LCD_WIDTH / 2, draw_width / 2, draw_height / 2, + cr, stride / 2, width / 2, height / 2); + video_draw_overlay(planes, paused, position_ms, duration_ms, + overlay_visible); + rb->lcd_blit_yuv(planes, 0, 0, LCD_WIDTH, 0, 0, + LCD_WIDTH, LCD_HEIGHT); +} + +static void video_redraw_frame(struct vpu_h264 *decoder, uint8_t *scaled, + bool paused, uint32_t position_ms, + uint32_t duration_ms, bool overlay_visible) +{ + const uint8_t *frame_y; + const uint8_t *frame_cb; + const uint8_t *frame_cr; + int width; + int height; + int stride; + + rb->hw_h264->get_frame(decoder, &frame_y, &frame_cb, &frame_cr, + &width, &height, &stride); + video_draw_frame(frame_y, frame_cb, frame_cr, width, height, stride, + scaled, paused, position_ms, duration_ms, + overlay_visible); +} + +int video_h264_play(const char *filepath, void *buffer, size_t buffer_size) +{ + static struct mp4v_demux_res demux; + static uint32_t probe_video_sample[1]; + static uint32_t probe_video_chunk[1]; + static uint32_t probe_video_sync[1]; + struct video_pool pool; + struct video_timing timing; + struct vpu_h264 *decoder = NULL; + struct mp4v_stsc_entry *video_stsc; + uint32_t *video_samples; + uint32_t *video_chunks; + uint32_t *video_sync; + uint8_t *decoder_buffer; + uint8_t *read_buffer; + uint8_t *scale_buffer; + void *audio_codec_workspace; + void *pcm_buffer; + size_t decoder_size; + size_t audio_codec_workspace_size; + int video_fd = -1; + uint32_t duration_ms; + uint32_t displayed_ms = 0; + uint32_t sample = 0; + uint32_t last_sample = 0; + long start_tick; + long pause_started = 0; + long overlay_until; + bool paused = false; + bool cpu_boosted = false; + bool have_audio = false; + bool have_frame = false; + bool overlay_drawn = false; + bool audio_master = false; + enum video_input_action action = VIDEO_INPUT_NONE; + int result = -1; + + if (filepath == NULL || filepath[0] == '\0' || buffer == NULL || + buffer_size == 0) + return -1; + + rb->splash(0, "Loading video..."); + rb->cpu_boost(true); + cpu_boosted = true; + rb->memset(&demux, 0, sizeof(demux)); + if (mp4v_demux_open(filepath, &demux, + probe_video_sample, 1, + probe_video_chunk, 1, NULL, 0, + probe_video_sync, 1) < 0) + { + rb->splash(HZ * 2, "Invalid MP4/M4V file"); + goto cleanup; + } + if (demux.format != MAKEFOURCC('a', 'v', 'c', '1') || + demux.avc_profile != 66 || demux.avc_level > VIDEO_MAX_LEVEL || + demux.width == 0 || demux.height == 0 || + demux.width > VIDEO_MAX_WIDTH || demux.height > VIDEO_MAX_HEIGHT || + demux.nalu_len_size < 1 || demux.nalu_len_size > 4) + { + rb->splash(HZ * 3, "Need H.264 Baseline <= L3.0\nMax 640x480"); + goto cleanup; + } + if (!video_tables_sane(&demux)) + { + rb->splash(HZ * 3, "Movie tables exceed safe limit"); + goto cleanup; + } + + pool.cursor = buffer; + pool.end = (uint8_t *)buffer + buffer_size; + video_samples = video_pool_take( + &pool, demux.num_samples * sizeof(*video_samples), 32); + video_chunks = video_pool_take( + &pool, demux.num_stco * sizeof(*video_chunks), 32); + video_stsc = video_pool_take( + &pool, demux.num_stsc * sizeof(*video_stsc), 32); + video_sync = demux.num_stss > 0 ? video_pool_take( + &pool, demux.num_stss * sizeof(*video_sync), 32) : NULL; + decoder_size = rb->hw_h264->buf_size( + (demux.width + 15) & ~15, (demux.height + 15) & ~15); + decoder_buffer = video_pool_take(&pool, decoder_size, 4096); + read_buffer = video_pool_take(&pool, VIDEO_READ_BUFFER, 32); + scale_buffer = video_pool_take(&pool, VIDEO_OUTPUT_BUFFER, 32); + audio_codec_workspace_size = demux.audio_num_samples > 0 ? + video_audio_workspace_size(&demux) : 0; + audio_codec_workspace = audio_codec_workspace_size > 0 ? + video_pool_take(&pool, audio_codec_workspace_size, + CACHEALIGN_SIZE) : NULL; + pcm_buffer = audio_codec_workspace_size > 0 ? + video_pool_take(&pool, VIDEO_PCM_BUFFER, CACHEALIGN_SIZE) : NULL; + if (video_samples == NULL || video_chunks == NULL || + video_stsc == NULL || + (demux.num_stss > 0 && video_sync == NULL) || + decoder_buffer == NULL || read_buffer == NULL || + scale_buffer == NULL || + (demux.audio_num_samples > 0 && + (audio_codec_workspace == NULL || pcm_buffer == NULL))) + { + rb->splash(HZ * 2, "Not enough video memory"); + goto cleanup; + } + rb->splash(0, "Loading movie index..."); + if (mp4v_demux_open(filepath, &demux, + video_samples, demux.num_samples, + video_chunks, demux.num_stco, + video_stsc, demux.num_stsc, + video_sync, demux.num_stss) < 0 || + !video_tables_sane(&demux) || !video_stsc_sane(&demux)) + { + rb->splash(HZ * 2, "MP4 table parse failed"); + goto cleanup; + } + duration_ms = video_duration_ms(&demux); + sample = video_resume_load(filepath, demux.num_samples); + while (sample > 0 && !mp4v_is_keyframe(&demux, sample)) + sample--; + video_timing_for_sample(&demux, sample, &timing); + + /* Preparation, decode, and LCD presentation need the 108 MHz bus clock. + * Keep one boost reference from table parsing through active playback. */ + rb->splash(0, "Starting video..."); + decoder = rb->hw_h264->open( + decoder_buffer, decoder_size, + (demux.width + 15) & ~15, (demux.height + 15) & ~15); + if (decoder == NULL || + rb->hw_h264->configure(decoder, demux.codecdata, + demux.codecdata_len) < 0) + { + rb->splash(HZ * 2, "H.264 decoder init failed"); + goto cleanup; + } + video_fd = rb->open(filepath, O_RDONLY); + if (video_fd < 0) + goto cleanup; + + rb->pcmbuf_fade(false, true); + rb->sound_set(SOUND_VOLUME, rb->global_status->volume); + start_tick = (*rb->current_tick) - + (long)((uint64_t)video_pts_ms(&demux, &timing) * HZ / 1000u); + overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + if (demux.audio_format == MAKEFOURCC('m', 'p', '4', 'a') && + demux.audio_codecdata_len > 0 && + video_audio_init(filepath, &demux, audio_codec_workspace, + audio_codec_workspace_size, pcm_buffer, + VIDEO_PCM_BUFFER) == 0) + { + int wait = 0; + uint32_t initial_ms = video_pts_ms(&demux, &timing); + + have_audio = true; + audio_master = true; + video_audio_play(); + if (initial_ms > 0) + video_audio_seek(initial_ms); + while (!video_audio_ready() && wait < HZ && action == VIDEO_INPUT_NONE) + { + bool redraw_requested; + + action = video_input(&paused, &start_tick, + &pause_started, true, &cpu_boosted, + &overlay_until, &redraw_requested); + if (action == VIDEO_INPUT_MENU) + action = video_show_menu( + &paused, &start_tick, &pause_started, true, + &cpu_boosted, &overlay_until, &redraw_requested); + rb->sleep(1); + wait++; + } + if (video_audio_failed()) + { + video_audio_stop(); + have_audio = false; + audio_master = false; + } + } + + rb->lcd_set_foreground(LCD_BLACK); + rb->lcd_clear_display(); + rb->lcd_update(); + rb->backlight_on(); + rb->backlight_set_timeout(0); + while (sample < demux.num_samples && action == VIDEO_INPUT_NONE) + { + uint32_t offset; + uint32_t size; + uint32_t pts_ms = video_pts_ms(&demux, &timing); + bool seek_requested = false; + int decoded; + + while (action == VIDEO_INPUT_NONE) + { + uint32_t clock_ms; + bool redraw_requested; + + action = video_input(&paused, &start_tick, + &pause_started, have_audio, &cpu_boosted, + &overlay_until, &redraw_requested); + if (action == VIDEO_INPUT_MENU) + action = video_show_menu( + &paused, &start_tick, &pause_started, have_audio, + &cpu_boosted, &overlay_until, &redraw_requested); + if (redraw_requested && have_frame) + { + video_redraw_frame(decoder, scale_buffer, paused, + displayed_ms, duration_ms, true); + overlay_drawn = true; + } + if (action == VIDEO_INPUT_SEEK_BACK || + action == VIDEO_INPUT_SEEK_FORWARD) + { + int64_t target = + (int64_t)(audio_master ? video_pcm_get_clock_ms() : + pts_ms) + + (action == VIDEO_INPUT_SEEK_BACK ? -10000 : 10000); + + if (target < 0) + target = 0; + if ((uint64_t)target >= duration_ms && duration_ms > 0) + target = duration_ms - 1; + sample = video_sample_for_ms(&demux, (uint32_t)target, + &timing); + pts_ms = video_pts_ms(&demux, &timing); + rb->hw_h264->close(decoder); + decoder = rb->hw_h264->open( + decoder_buffer, decoder_size, + (demux.width + 15) & ~15, + (demux.height + 15) & ~15); + if (decoder == NULL || + rb->hw_h264->configure(decoder, demux.codecdata, + demux.codecdata_len) < 0) + { + rb->splash(HZ * 2, "H.264 seek failed"); + decoder = NULL; + goto cleanup; + } + if (have_audio) + video_audio_seek(pts_ms); + start_tick = (*rb->current_tick) - + (long)((uint64_t)pts_ms * HZ / 1000u); + overlay_until = (*rb->current_tick) + VIDEO_OSD_SHOW_TICKS; + action = VIDEO_INPUT_NONE; + seek_requested = true; + break; + } + if (action != VIDEO_INPUT_NONE) + break; + if (paused) + { + if (overlay_drawn && + !TIME_BEFORE((*rb->current_tick), overlay_until)) + { + video_redraw_frame(decoder, scale_buffer, true, + displayed_ms, duration_ms, false); + overlay_drawn = false; + } + rb->sleep(1); + continue; + } + clock_ms = audio_master ? video_pcm_get_clock_ms() : + (uint32_t)(((*rb->current_tick) - start_tick) * 1000 / HZ); + if (audio_master && + (video_audio_failed() || + (!video_audio_is_active() && video_pcm_empty()))) + { + video_audio_stop(); + have_audio = false; + audio_master = false; + start_tick = (*rb->current_tick) - + (long)((uint64_t)clock_ms * HZ / 1000u); + } + if (clock_ms + 2 >= pts_ms) + break; + rb->sleep(1); + } + if (action != VIDEO_INPUT_NONE) + break; + if (seek_requested) + continue; + if (mp4v_get_sample_offset(&demux, sample, &offset, &size) < 0 || + size == 0 || size > VIDEO_READ_BUFFER || + rb->lseek(video_fd, offset, SEEK_SET) < 0 || + rb->read(video_fd, read_buffer, size) != (ssize_t)size) + { + rb->splash(HZ * 2, "Video sample read failed"); + goto cleanup; + } + decoded = rb->hw_h264->decode_sample( + decoder, read_buffer, size, demux.nalu_len_size); + if (decoded < 0) + { + rb->splash(HZ * 2, "VPU sample decode failed"); + goto cleanup; + } + if (decoded > 0) + { + const uint8_t *frame_y; + const uint8_t *frame_cb; + const uint8_t *frame_cr; + int width; + int height; + int stride; + bool overlay_visible = + TIME_BEFORE((*rb->current_tick), overlay_until); + + rb->hw_h264->get_frame(decoder, &frame_y, &frame_cb, &frame_cr, + &width, &height, &stride); + video_draw_frame(frame_y, frame_cb, frame_cr, width, height, + stride, scale_buffer, paused, pts_ms, + duration_ms, overlay_visible); + displayed_ms = pts_ms; + have_frame = true; + overlay_drawn = overlay_visible; + } + last_sample = sample; + video_timing_advance(&demux, &timing); + sample++; + } + result = action == VIDEO_INPUT_USB ? 2 : + (action == VIDEO_INPUT_EXIT ? 1 : 0); + if (result == 0) + video_resume_clear(filepath); + else + video_resume_save(filepath, last_sample, demux.num_samples); + +cleanup: + if (have_audio) + video_audio_stop(); + if (video_fd >= 0) + rb->close(video_fd); + if (decoder != NULL) + rb->hw_h264->close(decoder); + rb->pcmbuf_fade(false, false); + rb->backlight_set_timeout(rb->global_settings->backlight_timeout); + rb->lcd_set_foreground(LCD_BLACK); + rb->lcd_clear_display(); + rb->lcd_update(); + if (cpu_boosted) + rb->cpu_boost(false); + return result; +} + +#endif /* HAVE_HW_H264 */ diff --git a/apps/plugins/h264_player/video_playback.h b/apps/plugins/h264_player/video_playback.h new file mode 100644 index 0000000000..2433c44396 --- /dev/null +++ b/apps/plugins/h264_player/video_playback.h @@ -0,0 +1,21 @@ +/*************************************************************************** + * Hardware H.264 player implementation. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ +#ifndef VIDEO_PLAYBACK_H +#define VIDEO_PLAYBACK_H + +#include + +/* Play a validated H.264 Baseline/AAC-LC MP4 from caller-owned memory. + * The caller must retain the plugin audio buffer until this function returns. + * Returns 0 at end of stream, 1 on user exit, 2 for USB, or -1 on failure. */ +int video_h264_play(const char *filepath, void *buffer, size_t buffer_size); + +#endif /* VIDEO_PLAYBACK_H */ diff --git a/apps/plugins/viewers.config b/apps/plugins/viewers.config index d745a29cfe..151b97ad7c 100644 --- a/apps/plugins/viewers.config +++ b/apps/plugins/viewers.config @@ -76,6 +76,11 @@ wav,viewers/wavview,10 wav,viewers/wav2wv,- wav,viewers/mp3_encoder,- wav,viewers/test_codec,- +#ifdef HAVE_HW_H264 +mp4,viewers/h264_player,4 +m4v,viewers/h264_player,4 +mov,viewers/h264_player,4 +#endif mpg,viewers/mpegplayer,4 mpeg,viewers/mpegplayer,4 mpv,viewers/mpegplayer,4 diff --git a/docs/CREDITS b/docs/CREDITS index edb859249c..24b0feb054 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -767,6 +767,7 @@ Michael McAllister Hemant Kumar Jerry Shen Olivier Senn +David Cormier The libmad team The wavpack team diff --git a/firmware/SOURCES b/firmware/SOURCES index 8b524edcc4..02a9d7cd83 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1680,6 +1680,10 @@ target/arm/s5l8702/ipod6g/adc-6g.c target/arm/s5l8702/ipod6g/powermgmt-6g.c target/arm/s5l8702/ipod6g/power-6g.c target/arm/s5l8702/ipod6g/storage_ata-6g.c +#if defined(HAVE_HW_H264) && !defined(BOOTLOADER) +target/arm/s5l8702/ipod6g/vpu-6g.c +target/arm/s5l8702/ipod6g/vpu-h264-6g.c +#endif #if (CONFIG_RTC == RTC_NANO2G) target/arm/s5l8702/ipod6g/rtc-6g.c #endif diff --git a/firmware/export/config/ipod6g.h b/firmware/export/config/ipod6g.h index a3ade43de5..c25192526f 100644 --- a/firmware/export/config/ipod6g.h +++ b/firmware/export/config/ipod6g.h @@ -58,6 +58,11 @@ /* define this to enable JPEG decoding */ #define HAVE_JPEG +/* S5L8702 VPU-B hardware H.264 decoder. */ +#ifndef SIMULATOR +#define HAVE_HW_H264 +#endif + /* define this if you can invert the colours on your LCD */ //#define HAVE_LCD_INVERT diff --git a/firmware/export/hw_h264.h b/firmware/export/hw_h264.h new file mode 100644 index 0000000000..d7a403284f --- /dev/null +++ b/firmware/export/hw_h264.h @@ -0,0 +1,39 @@ +/*************************************************************************** + * Hardware H.264 decoder interface exposed to viewer plugins. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + ****************************************************************************/ + +#ifndef HW_H264_H +#define HW_H264_H + +#include +#include + +struct vpu_h264; + +/* The container, audio, synchronization, and UI layers live in the plugin. + * Only the target hardware decoder is exported by core. */ +struct hw_h264_api +{ + size_t (*buf_size)(int max_w, int max_h); + struct vpu_h264 *(*open)(void *buf, size_t buf_size, + int max_w, int max_h); + int (*configure)(struct vpu_h264 *decoder, + const uint8_t *avcc, int avcc_len); + int (*decode_sample)(struct vpu_h264 *decoder, const uint8_t *sample, + int sample_len, int nalu_len_size); + void (*get_frame)(const struct vpu_h264 *decoder, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride); + void (*close)(struct vpu_h264 *decoder); +}; + +extern const struct hw_h264_api target_hw_h264_api; + +#endif /* HW_H264_H */ diff --git a/firmware/export/s5l87xx.h b/firmware/export/s5l87xx.h index feb220286a..0ece79c0a6 100644 --- a/firmware/export/s5l87xx.h +++ b/firmware/export/s5l87xx.h @@ -1643,6 +1643,28 @@ The following peripherals are not present in the Samsung S5L8700 datasheet. Information for them was gathered solely by reverse-engineering Apple's firmware. */ +/* VPU-B H.264 decoder - S5L8702 */ +#if CONFIG_CPU == S5L8702 +#define VPU_MODE (*((REG32_PTR_T)(0x38100314))) +#define VPU_BASE 0x39800000 +#define VPU_REG(off) (*((REG32_PTR_T)(VPU_BASE + (off)))) + +#define VPU_DPB_Y(i) VPU_REG((i) * 12) +#define VPU_DPB_CB(i) VPU_REG((i) * 12 + 4) +#define VPU_DPB_CR(i) VPU_REG((i) * 12 + 8) +#define VPU_OUT_Y VPU_REG(0x0cc) +#define VPU_OUT_CB VPU_REG(0x0d0) +#define VPU_OUT_CR VPU_REG(0x0d4) +#define VPU_CTRL_BUF VPU_REG(0x0d8) +#define VPU_SLICE_DESC VPU_REG(0x0dc) +#define VPU_DIMS VPU_REG(0x0e0) +#define VPU_STRIDES VPU_REG(0x0e4) +#define VPU_CTRL VPU_REG(0x0e8) +#define VPU_STATUS0 VPU_REG(0x0f0) +#define VPU_STATUS1 VPU_REG(0x0f4) +#define VPU_CONFIG VPU_REG(0x118) +#endif + /* Hardware AES crypto unit - S5L8701+ */ #if CONFIG_CPU==S5L8701 #define AES_BASE 0x39800000 diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c new file mode 100644 index 0000000000..b5295b7059 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.c @@ -0,0 +1,71 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * S5L8702 VPU-B IRQ 35 completion handler. + * + * IRQ 35 is VIC1 bit 3 and is asserted when VPU-B completes a decode. + * The handler captures the completion status, acknowledges the edge, and + * releases a semaphore for the decoder driver. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "system.h" +#include "kernel.h" +#include "s5l87xx.h" + +#define VPU_IRQ_BIT (1u << 3) + +static struct semaphore vpu_complete; +static volatile uint32_t vpu_last_status1; + +void vpu_irq_init(void) +{ + semaphore_init(&vpu_complete, 1, 0); + vpu_last_status1 = 0; + VIC1INTENCLEAR = VPU_IRQ_BIT; + VIC1EDGE0 |= VPU_IRQ_BIT; + VIC1EDGE1 = VPU_IRQ_BIT; +} + +void vpu_irq_arm(void) +{ + semaphore_wait(&vpu_complete, 0); + VIC1EDGE1 = VPU_IRQ_BIT; + VIC1INTENABLE = VPU_IRQ_BIT; +} + +int vpu_irq_wait(int timeout_ticks) +{ + int ret = semaphore_wait(&vpu_complete, timeout_ticks); + VIC1INTENCLEAR = VPU_IRQ_BIT; + return ret; +} + +uint32_t vpu_irq_status1(void) +{ + return vpu_last_status1; +} + +void ICODE_ATTR INT_IRQ35(void) +{ + vpu_last_status1 = VPU_STATUS1; + VIC1EDGE1 = VPU_IRQ_BIT; + VIC1INTENCLEAR = VPU_IRQ_BIT; + semaphore_release(&vpu_complete); +} diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h new file mode 100644 index 0000000000..a3a64e8aff --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-6g.h @@ -0,0 +1,48 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * S5L8702 VPU-B IRQ completion for H.264 hardware decoder. + * + * VPU-B asserts IRQ 35 (VIC1 bit 3, edge-triggered) on decode completion. + * These functions provide semaphore-based synchronous wait, replacing + * the sleep(200ms) polling approach. + * + * Copyright (C) 2025-2026 David Cormier + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef VPU_6G_H +#define VPU_6G_H + +#include + +/* Initialize VPU-B IRQ 35: setup semaphore, configure edge trigger. + * Call once before first decode. */ +void vpu_irq_init(void); + +/* Arm for next decode: drain stale semaphore signal, clear pending + * edge, enable IRQ 35. Call BEFORE triggering VPU decode. */ +void vpu_irq_arm(void); + +/* Wait for VPU decode completion. Blocks until ISR fires or timeout. + * Disables IRQ 35 after return. Returns OBJ_WAIT_TIMEDOUT on timeout, + * OBJ_WAIT_SUCCEEDED on success. */ +int vpu_irq_wait(int timeout_ticks); + +/* Get STATUS1 value captured by ISR during last completion. */ +uint32_t vpu_irq_status1(void); + +#endif /* VPU_6G_H */ diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c new file mode 100644 index 0000000000..b4c5c6ea06 --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.c @@ -0,0 +1,1081 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * S5L8702 VPU-B H.264 Baseline hardware decoder implementation. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" + +#ifdef HAVE_HW_H264 + +#include "system.h" +#include "kernel.h" +#include "s5l87xx.h" +#include "vpu-h264-6g.h" +#include "vpu-6g.h" + +#include + +#define VPU_CONFIG_CONST 0x82625A00 +#define VPU_SLICE_CONST 0x00110C85 +#define VPU_TRIGGER_BITS 0x88003001 +#define VPU_MODE_H264 (1u << 0) +#define VPU_CLOCK_GATE_BIT (1u << 17) +#define VPP_CLOCK_GATE_MASK (7u << 14) + +#define ALIGN32(x) CACHEALIGN_UP(x) +#define ALIGN4K(x) ALIGN_UP((x), 0x1000) +#define PHYS(x) ((uint32_t)(uintptr_t)S5L8702_PHYSICAL_ADDR(x)) +#define UNCACHED(x) S5L8702_UNCACHED_ADDR(x) + +#define BS_DMA_SIZE 262144 +#define SLICE_DESC_SIZE 320 +#define SLICE_DESC_WORDS 8 +#define VPU_MAX_SLICES \ + (SLICE_DESC_SIZE / (SLICE_DESC_WORDS * sizeof(uint32_t))) + +/* ---- Bitstream reader (exp-Golomb) ---- */ + +struct bs { + const uint8_t *buf; + unsigned long bit_offset; + unsigned long bit_length; + int error; +}; + +static uint32_t bs_un(struct bs *b, int n) +{ + uint32_t val = 0; + int i; + + if (n < 0 || n > 32) + { + b->error = 1; + return 0; + } + for (i = 0; i < n; i++) + { + if (b->bit_offset >= b->bit_length) + { + b->error = 1; + return 0; + } + unsigned long byte_pos = b->bit_offset >> 3; + unsigned int bit_pos = 7 - (b->bit_offset & 7); + val = (val << 1) | ((b->buf[byte_pos] >> bit_pos) & 1); + b->bit_offset++; + } + return val; +} + +static uint32_t bs_u1(struct bs *b) { return bs_un(b, 1); } +static uint32_t bs_u8(struct bs *b) { return bs_un(b, 8); } + +static uint32_t bs_ue(struct bs *b) +{ + int lz = 0; + while (bs_u1(b) == 0 && lz < 31) + lz++; + if (b->error || lz >= 31) + { + b->error = 1; + return 0; + } + if (lz == 0) + return 0; + if (b->bit_offset >= b->bit_length) + { + b->error = 1; + return 0; + } + return (UINT32_C(1) << lz) - 1 + bs_un(b, lz); +} + +static int bs_se(struct bs *b) +{ + uint32_t v = bs_ue(b); + return (v & 1) ? (int)((v + 1) >> 1) : -(int)(v >> 1); +} + +/* ---- EBSP to RBSP conversion ---- */ + +static int ebsp_to_rbsp(uint8_t *dst, const uint8_t *src, int src_len) +{ + int di = 0, si; + for (si = 0; si < src_len; si++) + { + if (si + 2 < src_len && src[si] == 0 && src[si+1] == 0 && + src[si+2] == 3) + { + dst[di++] = 0; + dst[di++] = 0; + si += 2; + } + else + { + dst[di++] = src[si]; + } + } + return di; +} + +/* Map an RBSP byte offset back to the corresponding EBSP byte offset. + * The VPU expects EBSP data (handles EPBs internally), so after parsing + * the slice header in RBSP space we need to find where the slice body + * starts in the original EBSP stream. */ +static int map_rbsp_to_ebsp(const uint8_t *ebsp, int ebsp_len, int rbsp_pos) +{ + int ri = 0, si = 0; + while (si < ebsp_len && ri < rbsp_pos) + { + if (si + 2 < ebsp_len && + ebsp[si] == 0 && ebsp[si+1] == 0 && ebsp[si+2] == 3) + { + if (ri + 1 >= rbsp_pos) + return si + (rbsp_pos - ri); + ri += 2; + si += 3; + } + else + { + ri++; + si++; + } + } + return si; +} + +/* ---- H.264 header structures ---- */ + +struct sps { + int profile_idc, level_idc; + int pic_width_in_mbs_minus1, pic_height_in_map_units_minus1; + int log2_max_frame_num_minus4; + int pic_order_cnt_type, log2_max_pic_order_cnt_lsb_minus4; + int max_num_ref_frames; + int gaps_allowed; + int frame_mbs_only; + int crop_left, crop_right, crop_top, crop_bottom; +}; + +struct pps { + int pic_init_qp_minus26, chroma_qp_index_offset; + int deblocking_filter_control_present_flag, weighted_pred_flag; + int num_ref_idx_l0_default_active_minus1; + int entropy_coding_mode; + int bottom_field_pic_order; + int num_slice_groups_minus1; + int weighted_bipred_idc; +}; + +struct slice_hdr { + int first_mb_in_slice, slice_type, slice_qp_delta; + int disable_deblocking_filter_idc; + int alpha_c0_offset_div2, beta_offset_div2; + int frame_num; + int num_ref_idx_l0_active_minus1; + int ref_list_modification; + int adaptive_ref_marking; + unsigned long bits_consumed; +}; + +/* ---- H.264 header parsers ---- */ + +static void parse_sps(struct sps *sps, struct bs *b) +{ + b->bit_offset = 8; + sps->profile_idc = bs_u8(b); + bs_un(b, 8); + sps->level_idc = bs_u8(b); + bs_ue(b); + + if (sps->profile_idc != 66) + { + b->error = 1; + return; + } + + sps->log2_max_frame_num_minus4 = bs_ue(b); + sps->pic_order_cnt_type = bs_ue(b); + if (sps->pic_order_cnt_type != 0) + { + b->error = 1; + return; + } + sps->log2_max_pic_order_cnt_lsb_minus4 = bs_ue(b); + sps->max_num_ref_frames = bs_ue(b); + sps->gaps_allowed = bs_u1(b); + sps->pic_width_in_mbs_minus1 = bs_ue(b); + sps->pic_height_in_map_units_minus1 = bs_ue(b); + sps->frame_mbs_only = bs_u1(b); + if (!sps->frame_mbs_only) + bs_u1(b); + bs_u1(b); /* direct_8x8_inference_flag */ + sps->crop_left = 0; + sps->crop_right = 0; + sps->crop_top = 0; + sps->crop_bottom = 0; + if (bs_u1(b)) /* frame_cropping_flag */ + { + uint32_t crop_left = bs_ue(b); + uint32_t crop_right = bs_ue(b); + uint32_t crop_top = bs_ue(b); + uint32_t crop_bottom = bs_ue(b); + + /* Baseline profile is always progressive 4:2:0 here, so both crop + * units are two pixels. */ + if (crop_left > 0x7fff || crop_right > 0x7fff || + crop_top > 0x7fff || crop_bottom > 0x7fff) + { + b->error = 1; + return; + } + sps->crop_left = (int)crop_left * 2; + sps->crop_right = (int)crop_right * 2; + sps->crop_top = (int)crop_top * 2; + sps->crop_bottom = (int)crop_bottom * 2; + } +} + +static void parse_pps(struct pps *pps, struct bs *b) +{ + b->bit_offset = 8; + bs_ue(b); bs_ue(b); + pps->entropy_coding_mode = bs_u1(b); + pps->bottom_field_pic_order = bs_u1(b); + pps->num_slice_groups_minus1 = bs_ue(b); + pps->num_ref_idx_l0_default_active_minus1 = bs_ue(b); + bs_ue(b); + pps->weighted_pred_flag = bs_u1(b); + pps->weighted_bipred_idc = bs_un(b, 2); + pps->pic_init_qp_minus26 = bs_se(b); + bs_se(b); + pps->chroma_qp_index_offset = bs_se(b); + pps->deblocking_filter_control_present_flag = bs_u1(b); +} + +static bool pps_supported(const struct pps *pps) +{ + return !pps->entropy_coding_mode && + pps->num_slice_groups_minus1 == 0 && + !pps->weighted_pred_flag && pps->weighted_bipred_idc == 0 && + pps->num_ref_idx_l0_default_active_minus1 == 0 && + pps->pic_init_qp_minus26 >= -26 && + pps->pic_init_qp_minus26 <= 25 && + pps->chroma_qp_index_offset >= -12 && + pps->chroma_qp_index_offset <= 12; +} + +static void parse_slice_header(const struct sps *sps, const struct pps *pps, + struct bs *b, int nal_type, int nal_ref_idc, + struct slice_hdr *sh) +{ + b->bit_offset = 8; + sh->first_mb_in_slice = bs_ue(b); + sh->slice_type = bs_ue(b); + if (sh->slice_type >= 5) + sh->slice_type -= 5; + bs_ue(b); /* pic_parameter_set_id */ + sh->frame_num = bs_un(b, sps->log2_max_frame_num_minus4 + 4); + + if (nal_type == 5) + bs_ue(b); /* idr_pic_id */ + if (sps->pic_order_cnt_type == 0) + { + bs_un(b, sps->log2_max_pic_order_cnt_lsb_minus4 + 4); + /* QuickTime 7.6.6's iPod preset sets + * bottom_field_pic_order_in_frame_present_flag even though the + * sequence is progressive, and writes delta_pic_order_cnt_bottom. */ + if (pps->bottom_field_pic_order) + bs_se(b); + } + + sh->num_ref_idx_l0_active_minus1 = + pps->num_ref_idx_l0_default_active_minus1; + sh->ref_list_modification = 0; + sh->adaptive_ref_marking = 0; + if (sh->slice_type == 0 || sh->slice_type == 1) + { + if (bs_u1(b)) + { + sh->num_ref_idx_l0_active_minus1 = bs_ue(b); + if (sh->slice_type == 1) + bs_ue(b); + } + } + + /* ref_pic_list_modification */ + if (sh->slice_type == 0 || sh->slice_type == 1) + { + if (bs_u1(b)) + { + uint32_t idc; + sh->ref_list_modification = 1; + do { + idc = bs_ue(b); + if (idc == 0 || idc == 1) + bs_ue(b); + else if (idc == 2) + bs_ue(b); + } while (idc != 3 && b->bit_offset < b->bit_length); + } + if (sh->slice_type == 1) + { + if (bs_u1(b)) + { + uint32_t idc; + sh->ref_list_modification = 1; + do { + idc = bs_ue(b); + if (idc == 0 || idc == 1) + bs_ue(b); + else if (idc == 2) + bs_ue(b); + } while (idc != 3 && b->bit_offset < b->bit_length); + } + } + } + + /* dec_ref_pic_marking */ + if (nal_ref_idc != 0) + { + if (nal_type == 5) + { + bs_u1(b); bs_u1(b); + } + else + { + if (bs_u1(b)) + { + uint32_t mmco; + sh->adaptive_ref_marking = 1; + do { + mmco = bs_ue(b); + if (mmco == 1 || mmco == 3) + bs_ue(b); + if (mmco == 2) + bs_ue(b); + if (mmco == 3 || mmco == 6) + bs_ue(b); + if (mmco == 4) + bs_ue(b); + } while (mmco != 0); + } + } + } + + sh->slice_qp_delta = bs_se(b); + sh->disable_deblocking_filter_idc = 0; + sh->alpha_c0_offset_div2 = 0; + sh->beta_offset_div2 = 0; + + if (pps->deblocking_filter_control_present_flag) + { + sh->disable_deblocking_filter_idc = bs_ue(b); + if (sh->disable_deblocking_filter_idc != 1) + { + sh->alpha_c0_offset_div2 = bs_se(b); + sh->beta_offset_div2 = bs_se(b); + } + } + + sh->bits_consumed = b->bit_offset; +} + +/* ---- Slice descriptor builder ---- */ + +static void build_slice_descriptor(uint32_t *desc, + int first_mb, + int mb_count, + int slice_type, + int slice_qp_y, + int chroma_qp_off, + int weighted_pred, + int num_ref_l0, + int deblk_idc, + int alpha_off, + int beta_off, + int bit_offset, + uint32_t bs_phys_addr, + int bs_length) +{ + /* Word 0 carries the first macroblock and the number covered by this + * descriptor. Apple's iPod preset emits two half-picture slices. */ + desc[0] = ((first_mb & 0x7FF) << 21) + | ((mb_count & 0x7FF) << 10) + | ((slice_type & 0xF) << 6) + | (slice_qp_y & 0x3F); + + desc[1] = ((chroma_qp_off & 0xFF) << 15) + | ((weighted_pred & 1) << 14) + | ((num_ref_l0 & 0xF) << 10) + | ((deblk_idc & 0x3) << 8) + | ((alpha_off & 0xF) << 4) + | (beta_off & 0xF); + + desc[2] = VPU_SLICE_CONST; + desc[3] = 0; + desc[4] = bit_offset & 0xFF; + desc[5] = bs_phys_addr; + desc[6] = bs_length; + desc[7] = 0; +} + +/* ---- VPU-B power management ---- */ + +static void vpub_power_on(void) +{ + uint16_t cg; + uint32_t pw; + + /* Gate VPP clocks + VPU-B clock as initial state */ + VPU_MODE &= ~VPU_MODE_H264; + PWRCON(0) |= VPU_CLOCK_GATE_BIT | VPP_CLOCK_GATE_MASK; + /* No delay: Apple FW, all Rockbox drivers use zero delay after PWRCON */ + + /* Enable CG16_SVID (PLL2 source, already locked as system clock) */ + cg = CG16_SVID; + cg &= ~(CG16_DISABLE_BIT | (CG16_SEL_MSK << CG16_SEL_POS)); + cg |= CG16_SEL_PLL2 << CG16_SEL_POS; + CG16_SVID = cg; + + /* Enable VPP clocks */ + pw = PWRCON(0); + PWRCON(0) = pw & ~VPP_CLOCK_GATE_MASK; + + /* H.264 mode */ + VPU_MODE |= VPU_MODE_H264; + + /* Zero stale registers */ + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + { + volatile uint32_t *base = (volatile uint32_t *)VPU_BASE; + int i; + for (i = 0; i < 0x130/4; i++) + base[i] = 0; + } + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + + /* Initialize IRQ 35 for completion signaling */ + vpu_irq_init(); +} + +static void vpub_power_off(void) +{ + VPU_MODE &= ~VPU_MODE_H264; + PWRCON(0) |= VPU_CLOCK_GATE_BIT | VPP_CLOCK_GATE_MASK; +} + +/* ---- VPU-B decode trigger ---- */ + +static int vpub_decode(uint32_t ctrl_phys, uint32_t desc_phys, + uint32_t y_phys, uint32_t cb_phys, uint32_t cr_phys, + const uint32_t *dpb_y, const uint32_t *dpb_cb, + const uint32_t *dpb_cr, int dpb_count, + int width_mbs, int height_mbs, + int pic_w, int num_slices) +{ + uint32_t val; + int ret, old_irq; + + /* Double clock enable (Apple: 0x1c0b94 + 0x1c0ba4) */ + old_irq = disable_irq_save(); + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + old_irq = disable_irq_save(); + PWRCON(0) &= ~VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + + /* Re-set VPU_MODE every frame (Apple: 0x1c0bac) */ + VPU_MODE |= VPU_MODE_H264; + + /* vtable[0x48] = h264_vpu_stop_and_reset (0x001c0b4c) */ + { + volatile uint32_t *base = (volatile uint32_t *)VPU_BASE; + uint32_t ec_val; + int i; + ec_val = base[0xEC/4]; + base[0xEC/4] = ec_val & ~1u; + ec_val = base[0xEC/4]; + (void)ec_val; + for (i = 0; i < 0x130/4; i++) + base[i] = 0; + } + + /* Program registers (Apple's exact order from FUN_001c06ac) */ + VPU_CTRL_BUF = ctrl_phys; + val = VPU_DIMS; + val = (val & ~0x3F00) | ((height_mbs & 0x3F) << 8); + VPU_DIMS = val; + val = VPU_DIMS; + val = (val & ~0x003F) | (width_mbs & 0x3F); + VPU_DIMS = val; + val = VPU_CTRL; + val = (val & ~0x0FFE) | ((width_mbs * height_mbs * 2) & 0xFFE); + VPU_CTRL = val; + VPU_SLICE_DESC = desc_phys; + val = VPU_STRIDES; + val = (val & ~0x01FF0000) | (((pic_w / 2) & 0x1FF) << 16); + VPU_STRIDES = val; + val = VPU_STRIDES; + val = (val & ~0x000003FF) | (pic_w & 0x3FF); + VPU_STRIDES = val; + VPU_OUT_Y = y_phys; + VPU_OUT_CB = cb_phys; + VPU_OUT_CR = cr_phys; + VPU_CONFIG = VPU_CONFIG_CONST; + + /* Fill the DPB reference frame table with reference frames + * (newest at slot 0). Remaining slots get slot 0's data. */ + { + int dpb; + for (dpb = 0; dpb < dpb_count && dpb < 17; dpb++) + { + VPU_DPB_Y(dpb) = dpb_y[dpb]; + VPU_DPB_CB(dpb) = dpb_cb[dpb]; + VPU_DPB_CR(dpb) = dpb_cr[dpb]; + } + for (; dpb < 17; dpb++) + { + VPU_DPB_Y(dpb) = dpb_count > 0 ? dpb_y[0] : 0; + VPU_DPB_CB(dpb) = dpb_count > 0 ? dpb_cb[0] : 0; + VPU_DPB_CR(dpb) = dpb_count > 0 ? dpb_cr[0] : 0; + } + } + + /* Slice count + trigger */ + val = VPU_CTRL; + val = (val & ~0x07FF0000) | ((num_slices & 0x7FF) << 16); + VPU_CTRL = val; + + /* Arm IRQ, trigger, wait */ + vpu_irq_arm(); + VPU_CTRL = VPU_CTRL | VPU_TRIGGER_BITS; + + ret = vpu_irq_wait(HZ / 2); + if (ret == OBJ_WAIT_TIMEDOUT) + { + old_irq = disable_irq_save(); + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + return -1; + } + + val = vpu_irq_status1(); + ret = 0; + if ((val << 3) >> 21) + ret = -2; + + if (ret == 0) + VPU_CONFIG = VPU_CONFIG_CONST; + + /* Disable clock */ + old_irq = disable_irq_save(); + PWRCON(0) |= VPU_CLOCK_GATE_BIT; + restore_irq(old_irq); + + return ret; +} + +/* ---- Internal context structure ---- */ + +#define MAX_DPB_FRAMES 5 /* up to 4 reference frames + 1 output */ + +struct vpu_h264 { + int max_w, max_h; + int pic_w, pic_h, pic_wmb, pic_hmb; + int display_w, display_h; + struct sps sps; + struct pps pps; + int have_sps, have_pps; + int cur_out; /* next output buffer index */ + int last_out; /* most recently decoded picture */ + int dpb_count; /* valid references in dpb_order */ + int dpb_order[MAX_DPB_FRAMES]; /* buffer indices, oldest first */ + uint8_t *ctrl_buf; + uint8_t *slice_desc; + uint8_t *bs_dma; + uint8_t *nalu_buf; + uint8_t *frame_y[MAX_DPB_FRAMES]; + uint8_t *frame_cb[MAX_DPB_FRAMES]; + uint8_t *frame_cr[MAX_DPB_FRAMES]; + int frame_y_size; + int frame_cb_size; + int frame_cr_size; +}; + +static int apply_sps_dimensions(struct vpu_h264 *v) +{ + int horizontal_crop; + int vertical_crop; + + if (v->sps.profile_idc != 66 || v->sps.level_idc > 30 || + v->sps.pic_order_cnt_type != 0 || + v->sps.log2_max_frame_num_minus4 < 0 || + v->sps.log2_max_frame_num_minus4 > 12 || + v->sps.log2_max_pic_order_cnt_lsb_minus4 < 0 || + v->sps.log2_max_pic_order_cnt_lsb_minus4 > 12 || + v->sps.max_num_ref_frames < 0 || + v->sps.max_num_ref_frames > 2 || v->sps.gaps_allowed || + !v->sps.frame_mbs_only || + v->sps.pic_width_in_mbs_minus1 < 0 || + v->sps.pic_width_in_mbs_minus1 >= v->max_w / 16 || + v->sps.pic_height_in_map_units_minus1 < 0 || + v->sps.pic_height_in_map_units_minus1 >= v->max_h / 16) + return -1; + + v->pic_wmb = v->sps.pic_width_in_mbs_minus1 + 1; + v->pic_hmb = v->sps.pic_height_in_map_units_minus1 + 1; + v->pic_w = v->pic_wmb * 16; + v->pic_h = v->pic_hmb * 16; + horizontal_crop = v->sps.crop_left + v->sps.crop_right; + vertical_crop = v->sps.crop_top + v->sps.crop_bottom; + v->display_w = v->pic_w - horizontal_crop; + v->display_h = v->pic_h - vertical_crop; + + if (horizontal_crop < 0 || horizontal_crop >= v->pic_w || + vertical_crop < 0 || vertical_crop >= v->pic_h || + v->display_w <= 0 || v->display_h <= 0 || + (v->display_w & 1) || (v->display_h & 1)) + return -1; + + v->frame_y_size = v->pic_w * v->pic_h; + v->frame_cb_size = (v->pic_w / 2) * (v->pic_h / 2); + v->frame_cr_size = v->frame_cb_size; + return 0; +} + +/* ---- Public API ---- */ + +size_t vpu_h264_buf_size(int max_w, int max_h) +{ + size_t sz = 0; + + if (max_w <= 0 || max_h <= 0 || max_w > 1280 || max_h > 720) + return 0; + sz += sizeof(struct vpu_h264) + 32; + sz += (size_t)max_w * max_h * 3 / 2 + 4096; /* ctrl_buf */ + sz += SLICE_DESC_SIZE + 32; + sz += BS_DMA_SIZE + 32; + sz += BS_DMA_SIZE + 32; /* nalu_buf */ + sz += ((size_t)max_w * max_h + 4096) * MAX_DPB_FRAMES; /* frame_y */ + sz += ((size_t)max_w * max_h / 4 + 32) * MAX_DPB_FRAMES; + sz += ((size_t)max_w * max_h / 4 + 32) * MAX_DPB_FRAMES; + return sz; +} + +struct vpu_h264 *vpu_h264_open(void *buf, size_t buf_size, + int max_w, int max_h) +{ + uint8_t *p = (uint8_t *)buf; + struct vpu_h264 *v; + int fy_sz, fc_sz, ctrl_sz; + + if (max_w <= 0 || max_h <= 0 || max_w > 1280 || max_h > 720) + return NULL; + if (!buf || buf_size < vpu_h264_buf_size(max_w, max_h)) + return NULL; + + v = (struct vpu_h264 *)ALIGN32(p); + p = (uint8_t *)v + sizeof(*v); + memset(v, 0, sizeof(*v)); + + v->max_w = max_w; + v->max_h = max_h; + + ctrl_sz = max_w * max_h * 3 / 2; + fy_sz = max_w * max_h; + fc_sz = max_w * max_h / 4; + + v->ctrl_buf = (uint8_t *)ALIGN4K(p); p = v->ctrl_buf + ctrl_sz; + v->slice_desc = (uint8_t *)ALIGN32(p); + p = v->slice_desc + SLICE_DESC_SIZE; + v->bs_dma = (uint8_t *)ALIGN32(p); p = v->bs_dma + BS_DMA_SIZE; + v->nalu_buf = (uint8_t *)ALIGN32(p); p = v->nalu_buf + BS_DMA_SIZE; + + { + int i; + for (i = 0; i < MAX_DPB_FRAMES; i++) + { + v->frame_y[i] = (uint8_t *)ALIGN4K(p); p = v->frame_y[i] + fy_sz; + v->frame_cb[i] = (uint8_t *)ALIGN32(p); p = v->frame_cb[i] + fc_sz; + v->frame_cr[i] = (uint8_t *)ALIGN32(p); p = v->frame_cr[i] + fc_sz; + } + } + + v->frame_y_size = fy_sz; + v->frame_cb_size = fc_sz; + v->frame_cr_size = fc_sz; + v->cur_out = 0; + v->dpb_count = 0; + + /* Zero control buffers */ + memset(v->ctrl_buf, 0, ctrl_sz); + memset(v->bs_dma, 0, BS_DMA_SIZE); + + /* Pre-fill frame buffers with neutral YCbCr (black) */ + { + int i; + for (i = 0; i < MAX_DPB_FRAMES; i++) + { + memset(v->frame_y[i], 0x10, fy_sz); + memset(v->frame_cb[i], 0x80, fc_sz); + memset(v->frame_cr[i], 0x80, fc_sz); + } + } + + /* Make all cached initialization visible before the VPU uses DMA. */ + commit_dcache(); + vpub_power_on(); + return v; +} + +int vpu_h264_configure(struct vpu_h264 *v, + const uint8_t *avcc, int avcc_len) +{ + int i, cnt, offset; + int nalu_len_size; + struct bs b; + + if (!v || !avcc || avcc_len < 7) + return -1; + + /* avcC structure: version(1) profile(1) compat(1) level(1) + * nalu_len_size(1) num_sps(1) [sps_len(2) sps_data]... + * num_pps(1) [pps_len(2) pps_data]... */ + nalu_len_size = (avcc[4] & 0x03) + 1; + (void)nalu_len_size; + + /* Parse SPS */ + cnt = avcc[5] & 0x1F; + offset = 6; + for (i = 0; i < cnt && offset + 2 <= avcc_len; i++) + { + int sps_len = (avcc[offset] << 8) | avcc[offset + 1]; + offset += 2; + if (sps_len < 1 || offset + sps_len > avcc_len || + (avcc[offset] & 0x9f) != 7) + return -1; + int rbsp_len = ebsp_to_rbsp(v->nalu_buf, avcc + offset, sps_len); + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_sps(&v->sps, &b); + + if (b.error || apply_sps_dimensions(v) < 0) + return -1; + v->have_sps = 1; + offset += sps_len; + } + + /* Parse PPS */ + if (offset >= avcc_len) + return -1; + cnt = avcc[offset++]; + for (i = 0; i < cnt && offset + 2 <= avcc_len; i++) + { + int pps_len = (avcc[offset] << 8) | avcc[offset + 1]; + offset += 2; + if (pps_len < 1 || offset + pps_len > avcc_len || + (avcc[offset] & 0x9f) != 8) + return -1; + int rbsp_len = ebsp_to_rbsp(v->nalu_buf, avcc + offset, pps_len); + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_pps(&v->pps, &b); + if (b.error || !pps_supported(&v->pps)) + return -1; + v->have_pps = 1; + offset += pps_len; + } + + return (v->have_sps && v->have_pps) ? 0 : -1; +} + +struct picture_slice { + const uint8_t *nalu; + int nalu_len; + int nal_type; + int nal_ref_idc; + struct slice_hdr header; +}; + +static int parse_picture_slice(struct vpu_h264 *v, const uint8_t *nalu, + int nalu_len, struct picture_slice *slice) +{ + struct bs b; + int rbsp_len; + + if (!v->have_sps || !v->have_pps || nalu_len < 1 || + (nalu[0] & 0x80) != 0) + return -1; + rbsp_len = ebsp_to_rbsp(v->nalu_buf, nalu, MIN(nalu_len, 256)); + slice->nalu = nalu; + slice->nalu_len = nalu_len; + slice->nal_type = v->nalu_buf[0] & 0x1f; + slice->nal_ref_idc = (v->nalu_buf[0] >> 5) & 3; + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + parse_slice_header(&v->sps, &v->pps, &b, slice->nal_type, + slice->nal_ref_idc, &slice->header); + if (b.error || + (slice->header.slice_type != 0 && slice->header.slice_type != 2) || + slice->header.num_ref_idx_l0_active_minus1 != 0 || + slice->header.ref_list_modification || + slice->header.adaptive_ref_marking || + slice->header.disable_deblocking_filter_idc < 0 || + slice->header.disable_deblocking_filter_idc > 2 || + slice->header.alpha_c0_offset_div2 < -6 || + slice->header.alpha_c0_offset_div2 > 6 || + slice->header.beta_offset_div2 < -6 || + slice->header.beta_offset_div2 > 6) + return -1; + return 0; +} + +static int decode_picture(struct vpu_h264 *v, struct picture_slice *slices, + int slice_count) +{ + uint32_t dpb_y[MAX_DPB_FRAMES], dpb_cb[MAX_DPB_FRAMES]; + uint32_t dpb_cr[MAX_DPB_FRAMES]; + int total_mbs = v->pic_wmb * v->pic_hmb; + int out_buf = v->cur_out; + int dpb_fill = 0; + int dma_used = 0; + int is_idr; + int ret; + int i; + + if (slice_count < 1 || slice_count > (int)VPU_MAX_SLICES || + slices[0].header.first_mb_in_slice != 0) + return -1; + is_idr = slices[0].nal_type == 5; + for (i = 0; i < slice_count; i++) + { + if ((slices[i].nal_type != 1 && slices[i].nal_type != 5) || + (slices[i].nal_type == 5) != is_idr || + slices[i].nal_ref_idc != slices[0].nal_ref_idc || + slices[i].header.frame_num != slices[0].header.frame_num || + slices[i].header.slice_type != slices[0].header.slice_type || + slices[i].header.first_mb_in_slice < 0 || + slices[i].header.first_mb_in_slice >= total_mbs || + (i > 0 && slices[i].header.first_mb_in_slice <= + slices[i - 1].header.first_mb_in_slice)) + return -1; + } + + if (is_idr) + v->dpb_count = 0; + for (i = v->dpb_count - 1; i >= 0 && dpb_fill < MAX_DPB_FRAMES; i--) + { + int bi = v->dpb_order[i]; + dpb_y[dpb_fill] = PHYS(v->frame_y[bi]); + dpb_cb[dpb_fill] = PHYS(v->frame_cb[bi]); + dpb_cr[dpb_fill] = PHYS(v->frame_cr[bi]); + dpb_fill++; + } + + memset(UNCACHED(v->slice_desc), 0, SLICE_DESC_SIZE); + for (i = 0; i < slice_count; i++) + { + struct slice_hdr *sh = &slices[i].header; + int next_mb = i + 1 < slice_count ? + slices[i + 1].header.first_mb_in_slice : total_mbs; + int mb_count = next_mb - sh->first_mb_in_slice; + int slice_qp = 26 + v->pps.pic_init_qp_minus26 + + sh->slice_qp_delta; + int hdr_bytes = sh->bits_consumed / 8; + int bit_off = sh->bits_consumed & 7; + int ebsp_hdr_offset = map_rbsp_to_ebsp( + slices[i].nalu, slices[i].nalu_len, hdr_bytes); + int dma_len = slices[i].nalu_len - ebsp_hdr_offset; + int dma_offset = (dma_used + 31) & ~31; + + if (mb_count <= 0 || slice_qp < 0 || slice_qp > 51 || + dma_len <= 0 || dma_offset > BS_DMA_SIZE - dma_len) + return -1; + memcpy(UNCACHED(v->bs_dma + dma_offset), + slices[i].nalu + ebsp_hdr_offset, dma_len); + build_slice_descriptor( + (uint32_t *)UNCACHED(v->slice_desc) + + i * SLICE_DESC_WORDS, + sh->first_mb_in_slice, mb_count, sh->slice_type, slice_qp, + v->pps.chroma_qp_index_offset, v->pps.weighted_pred_flag, + sh->num_ref_idx_l0_active_minus1, + sh->disable_deblocking_filter_idc, + sh->alpha_c0_offset_div2, sh->beta_offset_div2, + bit_off, PHYS(v->bs_dma + dma_offset), dma_len); + dma_used = dma_offset + dma_len; + } + + ret = vpub_decode( + PHYS(v->ctrl_buf), PHYS(v->slice_desc), + PHYS(v->frame_y[out_buf]), PHYS(v->frame_cb[out_buf]), + PHYS(v->frame_cr[out_buf]), dpb_y, dpb_cb, dpb_cr, dpb_fill, + v->pic_wmb, v->pic_hmb, v->pic_w, slice_count); + commit_discard_dcache(); + if (ret != 0) + return -1; + + v->last_out = out_buf; + if (slices[0].nal_ref_idc != 0 && v->sps.max_num_ref_frames > 0) + { + int max_ref = MIN(v->sps.max_num_ref_frames, + MAX_DPB_FRAMES - 1); + while (v->dpb_count >= max_ref) + { + for (i = 0; i < v->dpb_count - 1; i++) + v->dpb_order[i] = v->dpb_order[i + 1]; + v->dpb_count--; + } + v->dpb_order[v->dpb_count++] = out_buf; + } + v->cur_out = (out_buf + 1) % MAX_DPB_FRAMES; + return 1; +} + +int vpu_h264_decode_nalu(struct vpu_h264 *v, + const uint8_t *nalu, int nalu_len) +{ + struct picture_slice slice; + struct bs b; + int rbsp_len; + int nal_type; + + if (!v || !nalu || nalu_len < 1 || (nalu[0] & 0x80) != 0) + return -1; + rbsp_len = ebsp_to_rbsp(v->nalu_buf, nalu, MIN(nalu_len, 256)); + nal_type = v->nalu_buf[0] & 0x1f; + b.buf = v->nalu_buf; + b.bit_offset = 0; + b.bit_length = rbsp_len * 8; + b.error = 0; + + if (nal_type == 7) + { + parse_sps(&v->sps, &b); + if (b.error || apply_sps_dimensions(v) < 0) + return -1; + v->have_sps = 1; + return 0; + } + if (nal_type == 8) + { + parse_pps(&v->pps, &b); + if (b.error || !pps_supported(&v->pps)) + return -1; + v->have_pps = 1; + return 0; + } + if (nal_type == 1 || nal_type == 5) + { + if (parse_picture_slice(v, nalu, nalu_len, &slice) < 0) + return -1; + return decode_picture(v, &slice, 1); + } + return 0; +} + +int vpu_h264_decode_sample(struct vpu_h264 *v, const uint8_t *sample, + int sample_len, int nalu_len_size) +{ + struct picture_slice slices[VPU_MAX_SLICES]; + int slice_count = 0; + int position = 0; + + if (!v || !sample || sample_len <= 0 || + nalu_len_size < 1 || nalu_len_size > 4) + return -1; + while (position + nalu_len_size <= sample_len) + { + uint32_t nalu_len = 0; + int nal_type; + int i; + + for (i = 0; i < nalu_len_size; i++) + nalu_len = (nalu_len << 8) | sample[position + i]; + position += nalu_len_size; + if (nalu_len == 0 || nalu_len > (uint32_t)(sample_len - position)) + return -1; + if ((sample[position] & 0x80) != 0) + return -1; + nal_type = sample[position] & 0x1f; + if (nal_type == 1 || nal_type == 5) + { + if (slice_count >= (int)VPU_MAX_SLICES || + parse_picture_slice(v, sample + position, nalu_len, + &slices[slice_count]) < 0) + return -1; + slice_count++; + } + else if (nal_type == 7 || nal_type == 8) + { + if (vpu_h264_decode_nalu(v, sample + position, nalu_len) < 0) + return -1; + } + position += nalu_len; + } + if (position != sample_len) + return -1; + return slice_count > 0 ? decode_picture(v, slices, slice_count) : 0; +} + +void vpu_h264_get_frame(const struct vpu_h264 *v, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride) +{ + int luma_offset = v->sps.crop_top * v->pic_w + v->sps.crop_left; + int chroma_offset = (v->sps.crop_top / 2) * (v->pic_w / 2) + + v->sps.crop_left / 2; + + if (y) + *y = v->frame_y[v->last_out] + luma_offset; + if (cb) + *cb = v->frame_cb[v->last_out] + chroma_offset; + if (cr) + *cr = v->frame_cr[v->last_out] + chroma_offset; + if (w) + *w = v->display_w; + if (h) + *h = v->display_h; + if (stride) + *stride = v->pic_w; +} + +void vpu_h264_close(struct vpu_h264 *v) +{ + if (v) + vpub_power_off(); +} + +const struct hw_h264_api target_hw_h264_api = { + .buf_size = vpu_h264_buf_size, + .open = vpu_h264_open, + .configure = vpu_h264_configure, + .decode_sample = vpu_h264_decode_sample, + .get_frame = vpu_h264_get_frame, + .close = vpu_h264_close, +}; + +#endif /* HAVE_HW_H264 */ diff --git a/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h new file mode 100644 index 0000000000..b5f9b65ddf --- /dev/null +++ b/firmware/target/arm/s5l8702/ipod6g/vpu-h264-6g.h @@ -0,0 +1,84 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025-2026 David Cormier + * + * S5L8702 VPU-B H.264 Baseline hardware decoder. + * + * Drives the VPU-B block on iPod Classic 6G (S5L8702). + * Full hardware decode: CAVLC, dequant, IDCT, intra/inter prediction, + * motion compensation, and in-loop deblocking. + * + * Supports: H.264 Baseline profile, level <= 3.0, up to ten ordered slices + * per frame. The single-slice path is proven bit-perfect against ffmpeg; + * multi-slice support follows the same Apple VPU descriptor interface. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef VPU_H264_6G_H +#define VPU_H264_6G_H + +#include "hw_h264.h" + +#include +#include + +/* Returns minimum buffer size needed for given max dimensions. + * Caller must provide a contiguous buffer of at least this size + * to vpu_h264_open(). Accounts for alignment padding. */ +size_t vpu_h264_buf_size(int max_w, int max_h); + +/* Initialize decoder. Allocates internal buffers from buf via bump + * allocator. Powers on VPU-B hardware and IRQ. + * Returns context pointer (inside buf) or NULL on error. */ +struct vpu_h264 *vpu_h264_open(void *buf, size_t buf_size, + int max_w, int max_h); + +/* Feed avcC decoder configuration (from MP4 container). + * Parses SPS and PPS NALUs from the avcC blob. + * Returns 0 on success, -1 on error. */ +int vpu_h264_configure(struct vpu_h264 *v, + const uint8_t *avcc, int avcc_len); + +/* Feed one raw NALU (including nal_header byte, excluding length prefix). + * Accepts EBSP (emulation prevention bytes intact). + * Handles SPS (7), PPS (8), IDR slice (5), non-IDR slice (1) internally. + * For slice NALUs: triggers synchronous HW decode (~5ms). + * + * Returns: 1 = frame decoded (call vpu_h264_get_frame) + * 0 = NALU consumed (SPS/PPS/SEI, no frame output) + * -1 = error (STATUS1 bits or timeout) */ +int vpu_h264_decode_nalu(struct vpu_h264 *v, + const uint8_t *nalu, int nalu_len); + +/* Decode one length-prefixed AVC sample as a complete picture. All slices + * are submitted together, matching the two-slice files produced by iTunes' + * "Create iPod or iPhone Version" action. */ +int vpu_h264_decode_sample(struct vpu_h264 *v, const uint8_t *sample, + int sample_len, int nalu_len_size); + +/* Get pointers to last decoded frame planes (YCbCr 4:2:0 planar). + * Pointers are cached aliases; cache is already invalidated. + * stride is the coded luma pitch; w/h are the cropped display dimensions. + * Valid until the next decode call. */ +void vpu_h264_get_frame(const struct vpu_h264 *v, + const uint8_t **y, const uint8_t **cb, + const uint8_t **cr, int *w, int *h, int *stride); + +/* Shutdown decoder. Powers off VPU-B hardware. */ +void vpu_h264_close(struct vpu_h264 *v); + +#endif /* VPU_H264_6G_H */ diff --git a/firmware/target/arm/s5l8702/system-s5l8702.c b/firmware/target/arm/s5l8702/system-s5l8702.c index a706e81491..8b6a314ce9 100644 --- a/firmware/target/arm/s5l8702/system-s5l8702.c +++ b/firmware/target/arm/s5l8702/system-s5l8702.c @@ -110,6 +110,9 @@ static const struct clocking_mode clk_modes[] = /* cdiv hdiv hprat hsdiv */ /* CClk HClk PClk SM1Clk FPS */ { 1, 2, 2, 4 }, /* 216 108 54 27 42 */ #ifdef HAVE_ADJUSTABLE_CPU_FREQ + /* Preserve the qualified intermediate step used when changing between + * the normal and boosted clocks. */ + { 2, 2, 2, 4 }, /* 108 108 54 27 */ { 4, 4, 2, 2 }, /* 54 54 27 27 21 */ #endif }; diff --git a/manual/platform/ipod6g.tex b/manual/platform/ipod6g.tex index fc77e03a25..078d7a75c1 100644 --- a/manual/platform/ipod6g.tex +++ b/manual/platform/ipod6g.tex @@ -5,6 +5,7 @@ \edef\UseOption{\UseOption,HAVE_DISK_STORAGE} \edef\UseOption{\UseOption,HAVE_CAR_ADAPTER_MODE} \edef\UseOption{\UseOption,HAVE_COMPOSITE_VIDEO_OUT} +\edef\UseOption{\UseOption,HAVE_HW_H264} \edef\UseOption{\UseOption,ipod} \newcommand{\playerman}{Ipod} diff --git a/manual/plugins/h264_player.tex b/manual/plugins/h264_player.tex new file mode 100644 index 0000000000..fb530bd309 --- /dev/null +++ b/manual/plugins/h264_player.tex @@ -0,0 +1,26 @@ +\subsection{H.264 Player} + +The H.264 Player uses a supported player's hardware video decoder to play +H.264 video from \fname{.mp4}, \fname{.m4v}, and \fname{.mov} files. It +supports H.264 Baseline Profile through level 3.0 at resolutions up to +640x480. AAC-LC audio is supported. + +Select a supported file in the Rockbox \setting{File Browser} to start +playback. Playback resumes from the last saved position unless the previous +playback reached the end or the final five percent of the file. + +\begin{btnmap} + \ActionWpsPlay + & Pause / Resume\\ + \ActionWpsVolUp{} / \ActionWpsVolDown + & Adjust volume up / down\\ + \ActionWpsSkipPrev{} / \ActionWpsSkipNext + & Seek backward / forward by 10 seconds\\ + \ActionWpsMenu + & Open the H.264 Player menu\\ + \ActionWpsStop + & Exit the player\\ +\end{btnmap} + +The player menu can resume playback or exit the player. A resume point is +saved when playback is stopped before the final five percent of the file. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index 1da6c16b59..dd07a4eba1 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -182,6 +182,9 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} & \fname{.s3m, .stm, .stx, .ult,} & \\ & \fname{.uni, .umx, .xm} & \\ MPEG Player & \fname{.mpg, .mpeg, .mpv, .m2v} & \\ + } + \opt{HAVE_HW_H264}{ + H.264 Player & \fname{.mp4, .m4v, .mov} & \\ } MP3 Encoder & \fname{.wav} & x \\ Rockboy & \fname{.gb, .gbc} & \\ @@ -211,6 +214,8 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} \nopt{lowmem}{\input{plugins/mpegplayer.tex}} +\opt{HAVE_HW_H264}{\input{plugins/h264_player.tex}} + \input{plugins/mp3_encoder.tex} \opt{iriverh300,iriverh100,SANSA_FUZE_PAD,SANSA_E200_PAD,IPOD_4G_PAD,IPOD_3G_PAD%