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%