diff --git a/container/container_ffmpeg.c b/container/container_ffmpeg.c index 7a5687c..7dc7fcb 100755 --- a/container/container_ffmpeg.c +++ b/container/container_ffmpeg.c @@ -232,6 +232,7 @@ static int32_t aac_latm_software_decode = 0; static int32_t ac3_software_decode = 0; static int32_t eac3_software_decode = 0; static int32_t dts_software_decode = 0; +static int32_t truehd_software_decode = 0; static int32_t amr_software_decode = 1; static int32_t vorbis_software_decode = 1; static int32_t opus_software_decode = 1; @@ -294,6 +295,11 @@ void dts_software_decoder_set(const int32_t val) dts_software_decode = val; } +void truehd_software_decoder_set(const int32_t val) +{ + truehd_software_decode = val; +} + void amr_software_decoder_set(const int32_t val) { amr_software_decode = val; @@ -450,8 +456,9 @@ static char* Codec2Encoding(int32_t codec_id, int32_t media_type, uint8_t *extra return (wma_software_decode) ? "A_IPCM" : "A_WMA/PRO"; case AV_CODEC_ID_WMALOSSLESS: return "A_IPCM"; + case AV_CODEC_ID_TRUEHD: case AV_CODEC_ID_MLP: - return "A_IPCM"; + return truehd_software_decode ? "A_IPCM" : "A_TRUEHD"; case AV_CODEC_ID_RA_144: return "A_IPCM"; case AV_CODEC_ID_RA_288: @@ -749,12 +756,14 @@ static void FFMPEGThread(Context_t *context) } } reset_finish_timeout(); - /* - if (bufferSize > 0) - { - context->output->Command(context, OUTPUT_CLEAR, NULL); - } - */ + /* OUTPUT_CLEAR here was tried as a gstreamer FLUSH equivalent + * but this path is NOT the seek boundary — it runs every + * av_read_frame iteration that still passes the gate. On + * HLS stitched sources (PlutoTV) it fired on every stitcher + * segment boundary and starved the audio producer: vpts + * stuck for minutes with av_drift = -425000 ms. Leaving + * disabled. dream_audio's da_reset_anchor_locked() stays + * available for when a real seek-completed wiring exists. */ } else { @@ -1894,6 +1903,18 @@ int32_t container_ffmpeg_init_av_context(Context_t *context, char *filename, uin av_dict_set(&avio_opts, "reconnect_streamed", "1", 0); } + /* HLS-only; persistent keep-alive + larger recv buffer upset + * low-bitrate icecast otherwise. */ + if (strstr(filename, ".m3u")) + { + av_dict_set(&avio_opts, "recv_buffer_size", "4194304", 0); + av_dict_set(&avio_opts, "multiple_requests", "1", 0); + av_dict_set(&avio_opts, "live_start_index", "-3", 0); + av_dict_set(&avio_opts, "max_reload", "10", 0); + av_dict_set(&avio_opts, "http_persistent", "1", 0); + av_dict_set(&avio_opts, "http_multiple", "1", 0); + } + if( strncmp(filename, "http://127.0.0.1", 16) == 0 ) { /* when using with ArchivCZSK, then indicate DRM support */ @@ -1903,6 +1924,21 @@ int32_t container_ffmpeg_init_av_context(Context_t *context, char *filename, uin pavio_opts = &avio_opts; + /* Cap the probe for local files; avformat_open_input ignores the + * matching av_dict keys, so pre-allocate the context and set them + * directly. Network URIs keep the defaults. */ + { + const int is_local = (strstr(filename, "://") == NULL) + || (strncmp(filename, "file://", 7) == 0); + if (is_local) { + avContextTab[AVIdx] = avformat_alloc_context(); + if (avContextTab[AVIdx]) { + avContextTab[AVIdx]->probesize = 10000000; + avContextTab[AVIdx]->max_analyze_duration = 3000000; + } + } + } + if ((err = avformat_open_input(&avContextTab[AVIdx], filename, fmt, pavio_opts)) != 0) { if (rtmp_proto_impl == 0 && //err == AVERROR_UNKNOWN && diff --git a/main/exteplayer.c b/main/exteplayer.c index 713946b..1785d93 100755 --- a/main/exteplayer.c +++ b/main/exteplayer.c @@ -55,6 +55,7 @@ extern int ffmpeg_av_dict_set(const char *key, const char *value, int flags); extern void aac_software_decoder_set(const int32_t val); extern void aac_latm_software_decoder_set(const int32_t val); extern void dts_software_decoder_set(const int32_t val); +extern void truehd_software_decoder_set(const int32_t val); extern void wma_software_decoder_set(const int32_t val); extern void ac3_software_decoder_set(const int32_t val); extern void eac3_software_decoder_set(const int32_t val); @@ -890,24 +891,56 @@ int main(int argc, char* argv[]) } #ifdef HAVE_DREAMNEXTGEN - /* DreamNextGen (AMlogic dreamone/dreamtwo) has no kernel ES audio - * decoder — force every codec through libavcodec → libswresample so - * container_ffmpeg.c always emits A_IPCM (S16 PCM) into our ALSA - * sink in output/dream_audio.c. - * - * Must run AFTER ParseParams: serviceapp invokes us with `-a 0 -n 0` - * etc. which would otherwise clear the flags right after we set them. */ - printf("DreamAudio: forcing software decode for AAC/AC3/EAC3/DTS/MP3/WMA/Vorbis/Opus/AMR\n"); - aac_software_decoder_set(1); - aac_latm_software_decoder_set(1); - ac3_software_decoder_set(1); - eac3_software_decoder_set(1); - dts_software_decoder_set(1); - mp3_software_decoder_set(1); - wma_software_decoder_set(1); - vorbis_software_decoder_set(1); - opus_software_decoder_set(1); - amr_software_decoder_set(1); + /* No kernel ES audio decoder: force SW decode for every codec except + * those the user opted into passthrough for. Must run after ParseParams + * (serviceapp's -a/-n args would otherwise clear the flags again). */ + { + int pt_ac3 = 0, pt_eac3 = 0, pt_dts = 0, pt_truehd = 0; + FILE *f = fopen("/sys/class/audiodsp/digital_raw", "r"); + int digital_raw = 0; + if (f) { if (fscanf(f, "%d", &digital_raw) != 1) digital_raw = 0; fclose(f); } + if (digital_raw > 0) { + FILE *s = fopen("/etc/enigma2/settings", "r"); + char eac3_val[64] = "passthrough"; /* default-on for EAC3 */ + char dts_val[64] = ""; /* default-off for DTS */ + char truehd_val[64] = ""; /* default-off for TrueHD */ + if (s) { + char line[256]; + while (fgets(line, sizeof(line), s)) { + if (strncmp(line, "config.av.transcodeac3plus=", 27) == 0) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + snprintf(eac3_val, sizeof(eac3_val), "%s", line + 27); + } else if (strncmp(line, "config.av.dtshd=", 16) == 0) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + snprintf(dts_val, sizeof(dts_val), "%s", line + 16); + } else if (strncmp(line, "config.av.truehd=", 17) == 0) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + snprintf(truehd_val, sizeof(truehd_val), "%s", line + 17); + } + } + fclose(s); + } + pt_ac3 = 1; + pt_eac3 = (strcmp(eac3_val, "passthrough") == 0 + || strcmp(eac3_val, "use_hdmi_caps") == 0); + pt_dts = (dts_val[0] && strcmp(dts_val, "downmix") != 0); + pt_truehd = (truehd_val[0] == 0 /* default: try passthrough */ + || strcmp(truehd_val, "downmix") != 0); + } + printf("DreamAudio: SW decode (passthrough: ac3=%d eac3=%d dts=%d truehd=%d)\n", + pt_ac3, pt_eac3, pt_dts, pt_truehd); + aac_software_decoder_set(1); + aac_latm_software_decoder_set(1); + if (!pt_ac3) ac3_software_decoder_set(1); + if (!pt_eac3) eac3_software_decoder_set(1); + if (!pt_dts) dts_software_decoder_set(1); + if (!pt_truehd) truehd_software_decoder_set(1); + mp3_software_decoder_set(1); + wma_software_decoder_set(1); + vorbis_software_decoder_set(1); + opus_software_decoder_set(1); + amr_software_decoder_set(1); + } #endif ffmpeg_av_dict_set("fake_last_subtitle", "1", 0); @@ -970,7 +1003,15 @@ int main(int argc, char* argv[]) g_player->output->Command(g_player, OUTPUT_SET_BUFFER_SIZE, &linuxDvbBufferSizeMB); g_player->manager->video->Command(g_player, MANAGER_REGISTER_UPDATED_TRACK_INFO, UpdateVideoTrack); - if (strncmp(playbackFiles.szFirstFile, "rtmp", 4) && strncmp(playbackFiles.szFirstFile, "ffrtmp", 4)) + /* noprobe sets max_analyze_duration=1us so avformat_find_stream_info + * returns almost immediately. Fine for local files where SPS/PPS sit + * right in the first packet, fatal for HLS/DASH where codec params + * arrive in later segments — ffmpeg then reports "unspecified size" + * for h264 and the AML hw decoder gets no dimensions, no video. */ + if (strncmp(playbackFiles.szFirstFile, "rtmp", 4) && + strncmp(playbackFiles.szFirstFile, "ffrtmp", 6) && + strncmp(playbackFiles.szFirstFile, "http://", 7) && /* NOSONAR S5332 */ + strncmp(playbackFiles.szFirstFile, "https://", 8)) { g_player->playback->noprobe = 1; } diff --git a/output/dream_audio.c b/output/dream_audio.c index f860a7c..158f08d 100644 --- a/output/dream_audio.c +++ b/output/dream_audio.c @@ -23,29 +23,73 @@ #include #include #include +#include #include +#include #include +#include +#include +#include +#include +#include +#include + #include "common.h" #include "output.h" #include "debug.h" #include "misc.h" #include "pcm.h" +#include "manager.h" #define cERR_DREAMAUDIO_NO_ERROR 0 #define cERR_DREAMAUDIO_ERROR -1 -#define DA_DBG(fmt, ...) do { fprintf(stderr, "[dream_audio] " fmt "\n", ##__VA_ARGS__); } while (0) +/* Dual-log to stderr (consumed by serviceapp PlayerApp::stderrAvail) AND + * to /tmp/dream_audio.log. serviceapp filters non-JSON stderr lines from + * the e2 debug log, so the file is the only reliable place to grep for + * the sync timeline. */ +static void da_log_emit(const char *line); +#define DA_DBG(fmt, ...) do { \ + char _da_buf[512]; \ + int _da_n = snprintf(_da_buf, sizeof(_da_buf), "[dream_audio] " fmt, ##__VA_ARGS__); \ + fprintf(stderr, "%s\n", _da_buf); \ + if (_da_n > 0) da_log_emit(_da_buf); \ +} while (0) #define ALSA_DEVICE_DEFAULT "default" -#define ALSA_BUFFER_TIME_US 500000 /* 500 ms — matches lib/dvb/alsa.cpp */ #define ALSA_OPEN_MAX_RETRIES 5 #define ALSA_OPEN_RETRY_MS 50 +/* 1024 × 8 = 170ms @ 48k, matches /etc/asound.conf dmix slave. */ +#define ALSA_PERIOD_FRAMES_AT_48K 1024 +#define ALSA_NUM_PERIODS 8 +#define ALSA_PREBUFFER_MS 50 +#define ALSA_PREFILL_MS 100 #define TSYNC_ENABLE "/sys/class/tsync/enable" +#define TSYNC_MODE "/sys/class/tsync/mode" #define TSYNC_PTS_AUDIO "/sys/class/tsync/pts_audio" #define TSYNC_DISCONTINUE "/sys/class/tsync/discontinue" +/* kernel tsync mode values (TSYNC_MODE_*). */ +#define TSYNC_MODE_VMASTER 0 +#define TSYNC_MODE_AMASTER 1 +#define TSYNC_MODE_PCRMASTER 2 + +/* ----- IEC61937 / AML passthrough ------------------------------------- */ +#define SYNCWORD1 0xF872 +#define SYNCWORD2 0x4E1F +#define IEC61937_AC3 0x01 +#define IEC61937_DTS1 0x0B +#define IEC61937_DTS2 0x0C +#define IEC61937_DTS3 0x0D +#define IEC61937_EAC3 0x15 +#define SPDIF_AC3_BUF_BYTES 6144 +#define SPDIF_EAC3_BUF_BYTES 24576 +#define DIGITAL_RAW_PCM 0 +#define DIGITAL_RAW_SPDIF 1 +#define AML_DIGITAL_RAW_PATH "/sys/class/audiodsp/digital_raw" +#define AML_DIGITAL_CODEC_PATH "/sys/class/audiodsp/digital_codec" /* ----- state ---------------------------------------------------------- */ @@ -60,6 +104,88 @@ static unsigned long long da_current_pts = 0; static char *da_device_name = NULL; /* strdup'd on open, freed on close */ +static int da_saved_tsync_enable = -1; + +/* Userspace AV-sync state: anchor + sustained-lag recovery. */ +static int da_pts_video_fd = -1; +static int da_anchor_armed = 0; +static size_t da_skip_bytes_remaining = 0; +static int64_t da_last_reanchor_ms = 0; +static int64_t da_last_huge_gap_ms = 0; +static int64_t da_drift_outside_since_ms = 0; +static int64_t da_last_sync_log_ms = 0; +/* vpts-frozen suppression: when the kernel video decoder stalls (DASH + * segment underrun on hr-live etc.) pts_video stops advancing and the + * anchor would otherwise flush ALSA + push silence on every cycle. Skip + * destructive actions while frozen, audio keeps playing. */ +static int64_t da_last_seen_vpts = -1; +static int64_t da_last_vpts_change_ms = 0; +#define DA_VPTS_FROZEN_MS 2000 + +/* Active passthrough codec: 1=AC3, 2=EAC3, 3=DTS (IEC61937 bursts), + * 4=TrueHD, 5=DTS-HD MA (HBR via libavformat spdif muxer). */ +static int da_pt_codec = 0; +static int da_pt_saved_raw = -1; +static int da_pt_saved_codec = -1; +static int da_pt_saved_spdif_fmt = -1; +static uint16_t da_pt_spdif[SPDIF_EAC3_BUF_BYTES / 2]; +static int da_pt_eac3_index = 0; +static int da_pt_eac3_count = 0; + +/* HBR muxer state — emits whole 16-byte (8ch * S16) ALSA frames @ 192 kHz. */ +#define DA_HBR_AVIO_BUFSIZE (128 * 1024) +#define DA_HBR_OUT_INIT_CAP (128 * 1024) +static AVFormatContext *da_hbr_fmt = NULL; +static AVIOContext *da_hbr_avio = NULL; +static AVStream *da_hbr_stream = NULL; +static uint8_t *da_hbr_buf = NULL; +static size_t da_hbr_buf_size = 0; +static size_t da_hbr_buf_cap = 0; +static int da_hbr_header_written = 0; +static int64_t da_hbr_last_pts = 0; + +/* ----- Producer/consumer queue (DreamAudioWrite → consumer thread) + * + * DreamAudioWrite runs on FFMPEGThread (container_ffmpeg.c). Without this + * queue, snd_pcm_writei runs inline there, so any time ALSA fills up the + * FFMPEGThread blocks in writei → no new av_read_frame → ALSA drains → + * underrun → drift-loop fires silence prefill. dream_video.c already has + * dv_q / dv_consumer_main for the same reason. Mirror that here. + * + * Item payload is post-decode (PCM scaled S16) or pre-built IEC61937 burst + * (passthrough). Producer mallocs+memcpys data and pushes; consumer pops, + * runs drift correction + writei, then frees. */ +#define DA_Q_CAP 128 +#define DA_Q_PUSH_TIMEOUT_MS 500 /* bounded producer wait, then drop */ + +typedef struct { + uint8_t *data; + size_t size; + int64_t pts_90k; /* -1 = INVALID_PTS_VALUE in source */ + unsigned int rate; + unsigned int channels; + int is_passthrough; /* 0 = PCM, else value of da_pt_codec at push time */ + unsigned int pt_rate; /* 48000 for AC3/EAC3/DTS, 192000 for HBR */ + unsigned int pt_ch; /* 2 for AC3/EAC3/DTS, 8 for HBR */ +} da_qitem_t; + +static da_qitem_t da_q[DA_Q_CAP]; +static int da_q_head = 0; /* producer writes here */ +static int da_q_tail = 0; /* consumer reads here */ +static pthread_mutex_t da_q_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t da_q_nonemp = PTHREAD_COND_INITIALIZER; +static pthread_cond_t da_q_nonfull = PTHREAD_COND_INITIALIZER; +static pthread_t da_q_thread; +static int da_q_running = 0; +static int da_q_stop = 0; +static uint64_t da_q_dropped = 0; /* push failed (queue full) */ + +/* Forward decls — the lifecycle handlers (Open/Close/Stop/Flush/Switch) + * call these but they are defined further down with the queue helpers. */ +static void da_q_start(void); +static void da_q_shutdown(void); +static void da_q_drain(void); + /* ----- sysfs helpers -------------------------------------------------- */ static void da_write_sysfs(const char *path, const char *val) @@ -70,11 +196,28 @@ static void da_write_sysfs(const char *path, const char *val) fclose(f); } +static int da_read_sysfs_int(const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) return -1; + int v = -1; + if (fscanf(f, "%d", &v) != 1) v = -1; + fclose(f); + return v; +} + static void da_tsync_set_enabled(int on) { da_write_sysfs(TSYNC_ENABLE, on ? "1" : "0"); } +static void da_tsync_set_mode(int mode) +{ + char buf[8]; + snprintf(buf, sizeof(buf), "%d", mode); + da_write_sysfs(TSYNC_MODE, buf); +} + static void da_tsync_checkin_apts(uint32_t pts_90khz) { char buf[32]; @@ -87,6 +230,468 @@ static void da_tsync_signal_discontinuity(void) da_write_sysfs(TSYNC_DISCONTINUE, "1"); } +/* ----- Userspace AV-sync drift loop helpers (dream_alsa.c port) -------- */ + +static void da_log_emit(const char *line) +{ + static FILE *fp = NULL; + static int tried = 0; + if (!fp && !tried) { + tried = 1; + int fd = open("/tmp/dream_audio.log", // NOSONAR + O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW | O_CLOEXEC, + 0600); + if (fd >= 0) { + fp = fdopen(fd, "a"); + if (!fp) close(fd); + } + if (fp) setvbuf(fp, NULL, _IOLBF, 0); + } + if (!fp) return; + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + fprintf(fp, "%02d:%02d:%02d.%03ld %s\n", + tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000000, line); +} + +static int64_t da_monotonic_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int64_t da_read_pts_video(void) +{ + if (da_pts_video_fd < 0) { + da_pts_video_fd = open("/sys/class/tsync/pts_video", O_RDONLY | O_CLOEXEC); + if (da_pts_video_fd < 0) return -1; + } + char buf[32]; + if (lseek(da_pts_video_fd, 0, SEEK_SET) < 0) return -1; + ssize_t n = read(da_pts_video_fd, buf, sizeof(buf) - 1); + if (n <= 0) return -1; + buf[n] = 0; + return (int64_t)strtoll(buf, NULL, 0); +} + +/* Push N ms of silence into ALSA. Caller holds da_mutex. */ +static void da_push_silence_ms(int ms) +{ + if (!da_handle || !da_configured || ms <= 0) return; + const size_t fb = (size_t)da_channels * sizeof(int16_t); + if (fb == 0) return; + static const uint8_t sil[8192] = { 0 }; + snd_pcm_uframes_t frames_total = (snd_pcm_uframes_t)((int64_t)ms * da_rate / 1000); + snd_pcm_uframes_t chunk_frames = sizeof(sil) / fb; + while (frames_total > 0) { + snd_pcm_uframes_t n = frames_total > chunk_frames ? chunk_frames : frames_total; + snd_pcm_sframes_t w = snd_pcm_writei(da_handle, sil, n); + if (w < 0) { if (snd_pcm_recover(da_handle, (int)w, 1) < 0) break; continue; } + if (w == 0) { usleep(1000); continue; } + frames_total -= (snd_pcm_uframes_t)w; + } +} + +static void da_write_sysfs_int(const char *path, int v) +{ + char buf[16]; + snprintf(buf, sizeof(buf), "%d", v); + da_write_sysfs(path, buf); +} + +/* ----- enigma2 settings reader (lightweight, no e2 headers) ---------- */ + +static int da_read_enigma2_setting(const char *key, char *out, size_t out_sz) +{ + FILE *f = fopen("/etc/enigma2/settings", "r"); + if (!f) return 0; + char line[256]; + const size_t klen = strlen(key); + int hit = 0; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, key, klen) != 0) continue; + if (line[klen] != '=') continue; + const char *val = line + klen + 1; + char *nl = strchr((char *)val, '\n'); if (nl) *nl = 0; + snprintf(out, out_sz, "%s", val); + hit = 1; + break; + } + fclose(f); + return hit; +} + +/* ----- ALSA mixer "Audio spdif format" (enum) ------------------------- */ + +static int da_get_spdif_format(void) +{ + snd_ctl_t *ctl = NULL; + if (snd_ctl_open(&ctl, "hw:0", 0) != 0) return -1; + snd_ctl_elem_id_t *id; snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_t *val; snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, "Audio spdif format"); + snd_ctl_elem_value_set_id(val, id); + int rc = snd_ctl_elem_read(ctl, val); + int cur = (rc == 0) ? (int)snd_ctl_elem_value_get_enumerated(val, 0) : -1; + snd_ctl_close(ctl); + return cur; +} + +static void da_set_spdif_format(int enum_val) +{ + snd_ctl_t *ctl = NULL; + if (snd_ctl_open(&ctl, "hw:0", 0) != 0) return; + snd_ctl_elem_id_t *id; snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_t *val; snd_ctl_elem_value_alloca(&val); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, "Audio spdif format"); + snd_ctl_elem_value_set_id(val, id); + snd_ctl_elem_value_set_enumerated(val, 0, (unsigned int)enum_val); + snd_ctl_elem_write(ctl, val); + snd_ctl_close(ctl); +} + +/* ----- Passthrough decision ------------------------------------------ */ + +/* Returns 1 for AC3, 2 for EAC3, 3 for DTS — 0 if no passthrough. */ +static int da_passthrough_codec_for(const char *encoding) +{ + if (!encoding) return 0; + + /* digital_raw=0 → decode to PCM regardless of per-codec settings. */ + if (da_read_sysfs_int(AML_DIGITAL_RAW_PATH) <= 0) return 0; + + char val[64]; + if (strcmp(encoding, "A_AC3") == 0) { + /* No per-codec enigma2 toggle for plain AC3 — digital_raw is it. */ + return 1; + } + if (strcmp(encoding, "A_EAC3") == 0) { + if (!da_read_enigma2_setting("config.av.transcodeac3plus", val, sizeof(val))) + return 2; /* default: passthrough */ + if (strcmp(val, "passthrough") == 0) return 2; + if (strcmp(val, "use_hdmi_caps") == 0) return 2; + return 0; /* "force_ac3" / "multichannel" → SW decode */ + } + if (strcmp(encoding, "A_DTS") == 0) { + if (!da_read_enigma2_setting("config.av.dtshd", val, sizeof(val))) + return 0; + if (strcmp(val, "downmix") == 0) return 0; + return 3; /* "passthrough" / "use_hdmi_caps" / "force_ac3" */ + } + return 0; +} + +/* ----- IEC61937 burst builders ---------------------------------------- */ + +/* Pairwise byte-swap (codec frame BE → ALSA S16 LE). Inline since glibc's + * swab() needs _DEFAULT_SOURCE which the build doesn't define. */ +static void da_byteswap16(const uint8_t *src, uint8_t *dst, size_t n) +{ + size_t pairs = n >> 1; + for (size_t i = 0; i < pairs; i++) { + dst[2*i] = src[2*i + 1]; + dst[2*i + 1] = src[2*i]; + } + if (n & 1) dst[n - 1] = 0; +} + +static int da_pt_build_ac3(const uint8_t *data, int size, uint8_t **out_buf, size_t *out_size) +{ + if (SPDIF_AC3_BUF_BYTES < size + 8) return -1; + da_write_sysfs_int(AML_DIGITAL_CODEC_PATH, 2); + + uint16_t *out = da_pt_spdif; + out[0] = SYNCWORD1; + out[1] = SYNCWORD2; + out[2] = (uint16_t)(IEC61937_AC3 | (data[5] & 0x07) << 8); + out[3] = (uint16_t)(size * 8); + + da_byteswap16(data, (uint8_t *)(out + 4), (size_t)size); + memset((uint8_t *)(out + 4) + size, 0, SPDIF_AC3_BUF_BYTES - 8 - size); + + *out_buf = (uint8_t *)out; + *out_size = SPDIF_AC3_BUF_BYTES; + return 0; +} + +static int da_pt_build_eac3(const uint8_t *data, int size, uint8_t **out_buf, size_t *out_size) +{ + if (SPDIF_EAC3_BUF_BYTES < da_pt_eac3_index + size + 8) return -1; + + int repeat = 1; + int bsid = data[5] >> 3; + if (bsid > 10 && (data[4] & 0xc0) != 0xc0) { + static const uint8_t eac3_repeat[4] = { 6, 3, 2, 1 }; + repeat = eac3_repeat[(data[4] & 0x30) >> 4]; + } + + da_write_sysfs_int(AML_DIGITAL_CODEC_PATH, 4); + + uint16_t *out = da_pt_spdif; + da_byteswap16(data, (uint8_t *)(out + 4) + da_pt_eac3_index, (size_t)size); + da_pt_eac3_index += size; + if (++da_pt_eac3_count < repeat) { + *out_buf = NULL; + *out_size = 0; + return 0; + } + + out[0] = SYNCWORD1; + out[1] = SYNCWORD2; + out[2] = IEC61937_EAC3; + out[3] = (uint16_t)(da_pt_eac3_index * 8); + memset((uint8_t *)(out + 4) + da_pt_eac3_index, 0, + SPDIF_EAC3_BUF_BYTES - 8 - da_pt_eac3_index); + + *out_buf = (uint8_t *)out; + *out_size = SPDIF_EAC3_BUF_BYTES; + + da_pt_eac3_index = 0; + da_pt_eac3_count = 0; + return 0; +} + +static int da_pt_build_dts(const uint8_t *data, int size, uint8_t **out_buf, size_t *out_size) +{ + uint8_t nbs = (uint8_t)(((data[4] & 0x01) << 6) | ((data[5] >> 2) & 0x3f)); + int bsid, burst_sz; + switch (nbs) { + case 0x07: bsid = 0x0a; burst_sz = 1024; break; + case 0x0f: bsid = IEC61937_DTS1; burst_sz = 2048; break; + case 0x1f: bsid = IEC61937_DTS2; burst_sz = 4096; break; + case 0x3f: bsid = IEC61937_DTS3; burst_sz = 8192; break; + default: + bsid = 0x00; + if (nbs < 5) nbs = 127; + burst_sz = (nbs + 1) * 32 * 2 + 2; + break; + } + if (burst_sz < size + 8) return -1; + + da_write_sysfs_int(AML_DIGITAL_CODEC_PATH, 1); + + uint16_t *out = da_pt_spdif; + out[0] = SYNCWORD1; + out[1] = SYNCWORD2; + out[2] = (uint16_t)bsid; + out[3] = (uint16_t)(size * 8); + out[4] = 0x7FFE; + out[5] = 0x8001; + + da_byteswap16(data, (uint8_t *)(out + 4), (size_t)size); + memset((uint8_t *)(out + 4) + size, 0, burst_sz - 8 - size); + + *out_buf = (uint8_t *)out; + *out_size = (size_t)burst_sz; + return 0; +} + +/* ----- HBR (TrueHD / DTS-HD MA) muxer --------------------------------- */ + +static int da_sink_has_codec(const char *name, int require_192k) +{ + FILE *f = fopen("/sys/class/amhdmitx/amhdmitx0/aud_cap", "r"); + if (!f) return 0; + char line[256]; + int hit = 0; + const size_t nlen = strlen(name); + while (fgets(line, sizeof(line), f)) { + const char *p = line; + while (*p == ' ' || *p == '\t') ++p; + if (strncmp(p, name, nlen) == 0 && (p[nlen] == ',' || p[nlen] == ' ')) { + if (!require_192k || strstr(line, "192")) hit = 1; + break; + } + } + fclose(f); + return hit; +} + +static int da_read_truehd_disabled(void) +{ + char val[64]; + if (!da_read_enigma2_setting("config.av.truehd", val, sizeof(val))) + return 0; /* default: try HBR */ + return strcmp(val, "downmix") == 0; +} + +/* Returns target da_pt_codec for HBR or 0 if not HBR-eligible. */ +static int da_passthrough_hbr_for(const char *encoding) +{ + if (!encoding) return 0; + char val[64]; + + if (strcmp(encoding, "A_TRUEHD") == 0) { + if (da_read_truehd_disabled()) return 0; + return (da_sink_has_codec("TrueHD", 1) || da_sink_has_codec("MAT", 1)) + ? 4 : 0; + } + /* DTS only goes HBR when the sink advertises 192 kHz DTS-HD. */ + if (strcmp(encoding, "A_DTS") == 0) { + if (!da_read_enigma2_setting("config.av.dtshd", val, sizeof(val))) + return 0; + if (strcmp(val, "downmix") == 0) return 0; + return da_sink_has_codec("DTS-HD", 1) ? 5 : 0; + } + return 0; +} + +/* AVIO write callback (non-const buf to match the ffmpeg-ext signature). */ +static int da_hbr_write_cb(void *opaque, uint8_t *buf, int buf_size) +{ + (void)opaque; + size_t need = da_hbr_buf_size + (size_t)buf_size; + if (need > da_hbr_buf_cap) { + size_t ncap = da_hbr_buf_cap ? da_hbr_buf_cap * 2 : DA_HBR_OUT_INIT_CAP; + while (ncap < need) ncap *= 2; + uint8_t *nb = realloc(da_hbr_buf, ncap); + if (!nb) return AVERROR(ENOMEM); + da_hbr_buf = nb; + da_hbr_buf_cap = ncap; + } + memcpy(da_hbr_buf + da_hbr_buf_size, buf, buf_size); + da_hbr_buf_size += (size_t)buf_size; + return buf_size; +} + +static int da_hbr_start(int hbr_codec) +{ + enum AVCodecID strm; + int digital_codec_enum; + int mixer_fmt; + + if (hbr_codec == 4) { /* TrueHD */ + strm = AV_CODEC_ID_TRUEHD; + digital_codec_enum = 7; + mixer_fmt = 7; /* Audio spdif format: TrueHD */ + } else if (hbr_codec == 5) { /* DTS-HD MA */ + strm = AV_CODEC_ID_DTS; + digital_codec_enum = 5; + mixer_fmt = 8; /* DTS-HD MA */ + } else return -1; + + da_write_sysfs_int(AML_DIGITAL_CODEC_PATH, digital_codec_enum); + da_set_spdif_format(mixer_fmt); + + int rc = avformat_alloc_output_context2(&da_hbr_fmt, NULL, "spdif", NULL); + if (rc < 0 || !da_hbr_fmt) { + DA_DBG("HBR: alloc spdif muxer: %d", rc); + return -1; + } + da_hbr_stream = avformat_new_stream(da_hbr_fmt, NULL); + if (!da_hbr_stream) { + avformat_free_context(da_hbr_fmt); da_hbr_fmt = NULL; + return -1; + } + da_hbr_stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; + da_hbr_stream->codecpar->codec_id = strm; + da_hbr_stream->codecpar->sample_rate = 48000; + av_channel_layout_default(&da_hbr_stream->codecpar->ch_layout, + strm == AV_CODEC_ID_TRUEHD ? 8 : 6); + + uint8_t *avio_buf = av_malloc(DA_HBR_AVIO_BUFSIZE); + if (!avio_buf) { + avformat_free_context(da_hbr_fmt); da_hbr_fmt = NULL; + return -1; + } + da_hbr_avio = avio_alloc_context(avio_buf, DA_HBR_AVIO_BUFSIZE, 1, NULL, + NULL, da_hbr_write_cb, NULL); + if (!da_hbr_avio) { + av_free(avio_buf); + avformat_free_context(da_hbr_fmt); da_hbr_fmt = NULL; + return -1; + } + da_hbr_fmt->pb = da_hbr_avio; + da_hbr_fmt->flags |= AVFMT_FLAG_CUSTOM_IO; + + AVDictionary *opts = NULL; + if (strm == AV_CODEC_ID_DTS) + av_dict_set(&opts, "dtshd_rate", "192000", 0); + rc = avformat_write_header(da_hbr_fmt, &opts); + av_dict_free(&opts); + if (rc < 0) { + DA_DBG("HBR: avformat_write_header: %d", rc); + return -1; + } + da_hbr_header_written = 1; + da_hbr_buf_cap = DA_HBR_OUT_INIT_CAP; + da_hbr_buf = malloc(da_hbr_buf_cap); + if (!da_hbr_buf) return -1; + DA_DBG("HBR: started codec=%d (digital_codec=%d mixer=%d) 192k/8ch", + hbr_codec, digital_codec_enum, mixer_fmt); + return 0; +} + +static void da_hbr_stop(void) +{ + if (da_hbr_fmt && da_hbr_header_written) { + av_write_trailer(da_hbr_fmt); + da_hbr_header_written = 0; + } + if (da_hbr_avio) { + uint8_t *live = da_hbr_avio->buffer; + avio_context_free(&da_hbr_avio); + if (live) av_free(live); + } + if (da_hbr_fmt) { + avformat_free_context(da_hbr_fmt); + da_hbr_fmt = NULL; + } + da_hbr_stream = NULL; + free(da_hbr_buf); + da_hbr_buf = NULL; + da_hbr_buf_size = 0; + da_hbr_buf_cap = 0; +} + +/* Push one TrueHD / DTS-HD-MA codec packet through the muxer; returns + * buffer pointer + byte count of whole-ALSA-frame (16-byte) chunks ready + * to write. Tail bytes stay queued for the next push. */ +static int da_hbr_push(const uint8_t *data, int size, int64_t pts, + uint8_t **out_buf, size_t *out_size) +{ + if (!da_hbr_fmt) return -1; + AVPacket *pkt = av_packet_alloc(); + if (!pkt) return -1; + if (av_new_packet(pkt, size) < 0) { av_packet_free(&pkt); return -1; } + memcpy(pkt->data, data, size); + pkt->stream_index = da_hbr_stream->index; + pkt->pts = pts; + pkt->dts = pts; + + int rc = av_write_frame(da_hbr_fmt, pkt); + av_packet_free(&pkt); + if (rc < 0) { + char eb[128]; av_strerror(rc, eb, sizeof(eb)); + DA_DBG("HBR: av_write_frame: %s", eb); + return -1; + } + avio_flush(da_hbr_avio); + + /* Emit whole 16-byte (8 ch * S16) ALSA frames. */ + size_t frames = da_hbr_buf_size / 16; + if (frames == 0) { *out_buf = NULL; *out_size = 0; return 0; } + size_t bytes = frames * 16; + *out_buf = da_hbr_buf; + *out_size = bytes; + /* Note: caller writes from da_hbr_buf BEFORE we shift — the shift + * happens in da_hbr_consume() after ALSA write succeeds. */ + return 0; +} + +static void da_hbr_consume(size_t bytes) +{ + if (bytes > da_hbr_buf_size) bytes = da_hbr_buf_size; + if (bytes < da_hbr_buf_size) + memmove(da_hbr_buf, da_hbr_buf + bytes, da_hbr_buf_size - bytes); + da_hbr_buf_size -= bytes; +} + /* ----- ALSA wrappers (caller holds da_mutex) -------------------------- */ static int da_alsa_open_handle(void) @@ -135,50 +740,176 @@ static int da_alsa_setparams(unsigned int rate, unsigned int channels) if (da_alsa_open_handle() < 0) return -1; - int err = snd_pcm_set_params(da_handle, - SND_PCM_FORMAT_S16, - SND_PCM_ACCESS_RW_INTERLEAVED, - channels, - rate, - 1, - ALSA_BUFFER_TIME_US); + snd_pcm_hw_params_t *hw; + snd_pcm_hw_params_alloca(&hw); + unsigned int r = rate, ch = channels; + snd_pcm_hw_params_any(da_handle, hw); + snd_pcm_hw_params_set_access(da_handle, hw, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(da_handle, hw, SND_PCM_FORMAT_S16); + snd_pcm_hw_params_set_rate_near(da_handle, hw, &r, NULL); + snd_pcm_hw_params_set_channels_near(da_handle, hw, &ch); + + snd_pcm_uframes_t period_size = + ((snd_pcm_uframes_t)r * ALSA_PERIOD_FRAMES_AT_48K) / 48000; + snd_pcm_uframes_t buffer_size = period_size * ALSA_NUM_PERIODS; + snd_pcm_hw_params_set_period_size_near(da_handle, hw, &period_size, NULL); + snd_pcm_hw_params_set_buffer_size_near(da_handle, hw, &buffer_size); + + int err = snd_pcm_hw_params(da_handle, hw); if (err < 0) { - DA_DBG("set_params(rate=%u ch=%u): %s", rate, channels, snd_strerror(err)); + DA_DBG("hw_params(rate=%u ch=%u): %s", rate, channels, snd_strerror(err)); return -1; } + snd_pcm_hw_params_get_period_size(hw, &period_size, NULL); + snd_pcm_hw_params_get_buffer_size(hw, &buffer_size); + + snd_pcm_uframes_t start_threshold = (r * ALSA_PREBUFFER_MS) / 1000; + if (start_threshold > buffer_size) start_threshold = buffer_size * 2 / 3; + + /* sw_params: hold device in PREPARED until start_threshold queued; + * silence-pad on underrun instead of XRUN/EPIPE. */ + snd_pcm_sw_params_t *sw; + snd_pcm_sw_params_alloca(&sw); + snd_pcm_sw_params_current(da_handle, sw); + snd_pcm_uframes_t boundary = 0; + snd_pcm_sw_params_get_boundary(sw, &boundary); + snd_pcm_sw_params_set_start_threshold(da_handle, sw, start_threshold); + snd_pcm_sw_params_set_stop_threshold(da_handle, sw, boundary); + snd_pcm_sw_params_set_silence_threshold(da_handle, sw, period_size); + snd_pcm_sw_params_set_silence_size(da_handle, sw, period_size); + int sw_err = snd_pcm_sw_params(da_handle, sw); + if (sw_err < 0) DA_DBG("sw_params: %s", snd_strerror(sw_err)); + da_rate = rate; da_channels = channels; da_configured = 1; - DA_DBG("configured rate=%u ch=%u", rate, channels); + + /* prefill silence so dmix queue doesn't run near-empty on first writei. */ + { + const size_t fb = (size_t)channels * sizeof(int16_t); + static const uint8_t sil[8192] = { 0 }; + snd_pcm_uframes_t left = (snd_pcm_uframes_t)rate * ALSA_PREFILL_MS / 1000; + snd_pcm_uframes_t chunk = sizeof(sil) / (fb ? fb : 1); + while (left > 0 && chunk > 0) { + snd_pcm_uframes_t n = (left > chunk) ? chunk : left; + snd_pcm_sframes_t w = snd_pcm_writei(da_handle, sil, n); + if (w <= 0) { snd_pcm_prepare(da_handle); break; } + left -= (snd_pcm_uframes_t)w; + } + } + + DA_DBG("configured rate=%u ch=%u period=%lu buf=%lu prefill=%dms", + rate, channels, (unsigned long)period_size, + (unsigned long)buffer_size, ALSA_PREFILL_MS); return 0; } +/* Fixed −20 dB SW attenuation (Q15 0.1) to match DVB-broadcast + * loudness. Streaming sources peak near 0 dBFS; DVB audio is + * dialnorm-normalised, so without this they're ~20 dB hotter at the + * same user volume. */ +#define DA_PCM_GAIN_Q15 3277 + static int da_alsa_write(const uint8_t *data, size_t size) { if (!da_handle || !da_configured || !data || size == 0) return -1; const size_t frame_bytes = (size_t)da_channels * sizeof(int16_t); if (frame_bytes == 0 || (size % frame_bytes) != 0) return -1; + int16_t *scaled = (int16_t *)malloc(size); + if (!scaled) return -1; + + const size_t nsamples = size / sizeof(int16_t); + const int16_t *src = (const int16_t *)data; + for (size_t i = 0; i < nsamples; i++) { + int32_t s = ((int32_t)src[i] * DA_PCM_GAIN_Q15) >> 15; + if (s > 32767) s = 32767; + if (s < -32768) s = -32768; + scaled[i] = (int16_t)s; + } + snd_pcm_uframes_t frames = size / frame_bytes; size_t offset = 0; + int ret = (int)size; while (frames > 0) { - snd_pcm_sframes_t n = snd_pcm_writei(da_handle, data + offset, frames); + snd_pcm_sframes_t n = snd_pcm_writei(da_handle, + (const uint8_t *)scaled + offset, frames); if (n < 0) { int err = snd_pcm_recover(da_handle, (int)n, 0); if (err < 0) { DA_DBG("recover: %s", snd_strerror(err)); - return -1; + ret = -1; + break; } continue; } offset += (size_t)n * frame_bytes; frames -= (snd_pcm_uframes_t)n; } - return (int)size; + free(scaled); + return ret; } /* ----- Output_t Command handlers -------------------------------------- */ +static void da_passthrough_setup(Context_t *context) +{ + da_pt_codec = 0; + da_pt_eac3_index = 0; + da_pt_eac3_count = 0; + + if (!context || !context->manager || !context->manager->audio) return; + + char *encoding = NULL; + context->manager->audio->Command(context, MANAGER_GETENCODING, &encoding); + if (!encoding) return; + + /* Prefer HBR; fall back to plain IEC61937 burst. */ + int hbr = da_passthrough_hbr_for(encoding); + int pt = hbr ? hbr : da_passthrough_codec_for(encoding); + DA_DBG("passthrough probe: encoding='%s' -> codec=%d (hbr=%d)", + encoding, pt, hbr); + free(encoding); + if (!pt) return; + + /* Save AML state for restore on Close. */ + da_pt_saved_raw = da_read_sysfs_int(AML_DIGITAL_RAW_PATH); + da_pt_saved_codec = da_read_sysfs_int(AML_DIGITAL_CODEC_PATH); + da_pt_saved_spdif_fmt = da_get_spdif_format(); + + if (hbr) { + /* HBR: spdif format + digital_codec already set inside da_hbr_start. */ + if (da_hbr_start(hbr) < 0) { + DA_DBG("HBR start failed — falling back to PCM"); + da_hbr_stop(); + return; + } + } else { + /* Audio spdif format enum: 2=AC3, 4=EAC3, 3=DTS. */ + int mixer_enum = (pt == 1) ? 2 : (pt == 2) ? 4 : 3; + da_set_spdif_format(mixer_enum); + } + + da_pt_codec = pt; + DA_DBG("passthrough ON: codec=%d saved_raw=%d saved_codec=%d saved_fmt=%d", + pt, da_pt_saved_raw, da_pt_saved_codec, da_pt_saved_spdif_fmt); +} + +static void da_passthrough_restore(void) +{ + if (!da_pt_codec) return; + if (da_pt_codec >= 4) da_hbr_stop(); /* TrueHD / DTS-HD-MA */ + if (da_pt_saved_spdif_fmt >= 0) da_set_spdif_format(da_pt_saved_spdif_fmt); + if (da_pt_saved_codec >= 0) da_write_sysfs_int(AML_DIGITAL_CODEC_PATH, da_pt_saved_codec); + /* leave digital_raw alone — it's a global user toggle, not ours to flip */ + da_pt_codec = 0; + da_pt_saved_spdif_fmt = -1; + da_pt_saved_codec = -1; + da_pt_saved_raw = -1; + da_pt_eac3_index = 0; + da_pt_eac3_count = 0; +} + static int DreamAudioOpen(Context_t *context, char *type) { if (strcmp(type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; @@ -188,21 +919,57 @@ static int DreamAudioOpen(Context_t *context, char *type) const char *dev = getenv("EXTEPLAYER3_ALSA_DEVICE"); da_device_name = strdup(dev && *dev ? dev : ALSA_DEVICE_DEFAULT); } - /* Leave kernel tsync alone — dream_video relies on it for AV - * sync (audio-PTS via TSYNC_PTS_AUDIO writes below, video-PTS - * via the AMSTREAM_IOC_TSTAMP ioctl). For audio-only streams - * tsync's VMASTER default doesn't pause anything: ALSA owns - * its own timing here. */ + /* Match what gstplayer2 actually does (verified via strace) — kernel + * tsync ENABLED in PCRMASTER, pts_audio fed per chunk in Write. The + * stale "VMASTER = disable tsync" comment in dream_avsync.c does not + * reflect dreamaudiosink's runtime behaviour. */ + if (da_saved_tsync_enable < 0) + da_saved_tsync_enable = da_read_sysfs_int(TSYNC_ENABLE); + /* Wipe leftover pts from any previous owner (live-TV / earlier + * gstplayer / earlier exteplayer3 session) so pcrmaster anchors on + * the new stream rather than the carried-over clock. */ + da_write_sysfs("/sys/class/tsync/pts_audio", "0"); + da_write_sysfs("/sys/class/tsync/pts_video", "0"); + da_tsync_signal_discontinuity(); + /* Match dreamaudiosink (DEFAULT_TSYNC_MODE=2 in gstdreamaudiosink.c). + * PCRMASTER: kernel uses smoothed pcrscr (synthesized from our + * pts_audio writes) as the master clock. AMASTER caused the kernel + * pacer to release video frames at the audio-chunk arrival rate + * (~50fps for our 20ms chunks), dropping ~33% of frames for 25fps + * source — visible as the "gefesselt" stutter. */ + da_tsync_set_mode(TSYNC_MODE_PCRMASTER); + da_tsync_set_enabled(1); + /* gstdreamaudiosink.c:178 — pcr_offset=0x0 + auto_pcr_offset=0x0 + * snd_pcm_delay() deckt unsere komplette audio queue ab, deshalb keine + * extra HW-pipeline-latency-Kompensation gegen pts_video nötig. Ohne + * diese Writes bleibt pcr_offset auf dem von Live-TV's eAVSyncCore + * gesetzten Wert (75 ms) hängen → pcrscr drifted gegenüber pts_audio. */ + da_write_sysfs("/proc/stb/pcr_offset", "0x0"); + da_write_sysfs("/proc/stb/auto_pcr_offset", "0x0"); + /* Reset sync state; anchor fires on first chunk. */ + da_anchor_armed = 1; + da_skip_bytes_remaining = 0; + da_last_reanchor_ms = 0; + da_last_huge_gap_ms = 0; + da_drift_outside_since_ms = 0; + da_last_sync_log_ms = 0; + da_last_seen_vpts = -1; + da_last_vpts_change_ms = 0; int ret = da_alsa_open_handle(); da_paused = 0; da_running = 0; + da_passthrough_setup(context); pthread_mutex_unlock(&da_mutex); if (ret < 0) { DA_DBG("open failed"); return cERR_DREAMAUDIO_ERROR; } - DA_DBG("opened device='%s'", da_device_name); + + /* Start consumer thread last — it expects da_handle ready. */ + da_q_start(); + + DA_DBG("opened device='%s' passthrough=%d", da_device_name, da_pt_codec); return cERR_DREAMAUDIO_NO_ERROR; } @@ -210,12 +977,26 @@ static int DreamAudioClose(Context_t *context, char *type) { if (strcmp(type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; + /* Stop+join consumer before closing the ALSA handle, otherwise the + * worker could still be inside snd_pcm_writei. */ + da_q_shutdown(); + pthread_mutex_lock(&da_mutex); da_alsa_close_handle(); + da_passthrough_restore(); da_running = 0; da_paused = 0; da_current_pts = 0; pthread_mutex_unlock(&da_mutex); + + if (da_saved_tsync_enable >= 0) { + /* signal discontinuity so kernel drops stale pts_audio before + * the next owner (live-TV's eAVSyncCore) re-enables sync. */ + da_tsync_signal_discontinuity(); + da_tsync_set_enabled(da_saved_tsync_enable); + da_saved_tsync_enable = -1; + } + if (da_pts_video_fd >= 0) { close(da_pts_video_fd); da_pts_video_fd = -1; } DA_DBG("closed"); return cERR_DREAMAUDIO_NO_ERROR; } @@ -232,14 +1013,33 @@ static int DreamAudioPlay(Context_t *context, char *type) return cERR_DREAMAUDIO_NO_ERROR; } +/* Mirror of dream_alsa_reset_anchor() in the dreamaudiosink reference + * implementation. Called from Stop/Flush/Switch so the first chunk after + * a seek runs the one-shot anchor cleanly instead of fighting stale + * vpts/apts state through the HUGE-gap → silence-prefill cascade. + * Caller must hold da_mutex. */ +static void da_reset_anchor_locked(void) +{ + da_anchor_armed = 1; + da_skip_bytes_remaining = 0; + da_last_reanchor_ms = 0; + da_last_huge_gap_ms = 0; + da_drift_outside_since_ms = 0; + da_last_sync_log_ms = 0; + da_last_seen_vpts = -1; + da_last_vpts_change_ms = 0; +} + static int DreamAudioStop(Context_t *context, char *type) { if (strcmp(type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; + da_q_drain(); pthread_mutex_lock(&da_mutex); if (da_handle) { snd_pcm_drop(da_handle); snd_pcm_prepare(da_handle); } + da_reset_anchor_locked(); da_running = 0; da_current_pts = 0; pthread_mutex_unlock(&da_mutex); @@ -250,11 +1050,13 @@ static int DreamAudioStop(Context_t *context, char *type) static int DreamAudioFlush(Context_t *context, char *type) { if (strcmp(type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; + da_q_drain(); pthread_mutex_lock(&da_mutex); if (da_handle) { snd_pcm_drop(da_handle); snd_pcm_prepare(da_handle); } + da_reset_anchor_locked(); da_tsync_signal_discontinuity(); pthread_mutex_unlock(&da_mutex); DA_DBG("flush"); @@ -288,9 +1090,22 @@ static int DreamAudioClear(Context_t *context, char *type) static int DreamAudioSwitch(Context_t *context, char *type) { - /* track switch — drop pending buffer so PCM from the new codec params - * doesn't get mixed with the tail of the old track */ - return DreamAudioFlush(context, type); + if (strcmp(type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; + /* track switch — drop pending buffer + re-evaluate passthrough since + * the new track may have a different codec */ + da_q_drain(); + pthread_mutex_lock(&da_mutex); + if (da_handle) { + snd_pcm_drop(da_handle); + snd_pcm_prepare(da_handle); + } + da_reset_anchor_locked(); + da_passthrough_restore(); + da_passthrough_setup(context); + pthread_mutex_unlock(&da_mutex); + da_tsync_signal_discontinuity(); + DA_DBG("switch (passthrough=%d)", da_pt_codec); + return cERR_DREAMAUDIO_NO_ERROR; } static int DreamAudioPts(Context_t *context, unsigned long long *pts) @@ -303,18 +1118,360 @@ static int DreamAudioPts(Context_t *context, unsigned long long *pts) /* ----- Write (the actual PCM hand-off) -------------------------------- */ +/* Direct ALSA write without the PCM gain (used for IEC61937 bursts; + * caller holds da_mutex). */ +static int da_alsa_write_raw(const uint8_t *data, size_t size) +{ + if (!da_handle || !da_configured || !data || size == 0) return -1; + const size_t frame_bytes = (size_t)da_channels * sizeof(int16_t); + if (frame_bytes == 0 || (size % frame_bytes) != 0) return -1; + + snd_pcm_uframes_t frames = size / frame_bytes; + size_t offset = 0; + while (frames > 0) { + snd_pcm_sframes_t n = snd_pcm_writei(da_handle, data + offset, frames); + if (n < 0) { + int err = snd_pcm_recover(da_handle, (int)n, 0); + if (err < 0) { DA_DBG("recover: %s", snd_strerror(err)); return -1; } + continue; + } + offset += (size_t)n * frame_bytes; + frames -= (snd_pcm_uframes_t)n; + } + return (int)size; +} + +/* ----- Producer/consumer queue helpers + consumer thread -------------- */ + +static int da_q_is_full_locked(void) { return ((da_q_head + 1) % DA_Q_CAP) == da_q_tail; } +static int da_q_is_empty_locked(void) { return da_q_head == da_q_tail; } + +static int da_q_depth_locked(void) +{ + return (da_q_head - da_q_tail + DA_Q_CAP) % DA_Q_CAP; +} + +/* Push: caller transfers data ownership into queue on success. + * Bounded wait DA_Q_PUSH_TIMEOUT_MS, then drop. + * Return 0 = pushed (caller must NOT free), -1 = dropped (caller still owns). */ +static int da_q_push(da_qitem_t *src) +{ + pthread_mutex_lock(&da_q_mu); + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += (long)DA_Q_PUSH_TIMEOUT_MS * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec += deadline.tv_nsec / 1000000000L; + deadline.tv_nsec %= 1000000000L; + } + while (da_q_is_full_locked() && !da_q_stop) { + if (pthread_cond_timedwait(&da_q_nonfull, &da_q_mu, &deadline) != 0) break; + } + if (da_q_is_full_locked() || da_q_stop) { + da_q_dropped++; + pthread_mutex_unlock(&da_q_mu); + return -1; + } + da_q[da_q_head] = *src; + da_q_head = (da_q_head + 1) % DA_Q_CAP; + pthread_cond_signal(&da_q_nonemp); + pthread_mutex_unlock(&da_q_mu); + return 0; +} + +/* Pop: blocks until item available or shutdown. Returns 0 with *out filled + * (consumer takes ownership of out->data) or -1 on shutdown with empty queue. */ +static int da_q_pop(da_qitem_t *out) +{ + pthread_mutex_lock(&da_q_mu); + while (da_q_is_empty_locked() && !da_q_stop) { + pthread_cond_wait(&da_q_nonemp, &da_q_mu); + } + if (da_q_stop && da_q_is_empty_locked()) { + pthread_mutex_unlock(&da_q_mu); + return -1; + } + *out = da_q[da_q_tail]; + da_q_tail = (da_q_tail + 1) % DA_Q_CAP; + pthread_cond_signal(&da_q_nonfull); + pthread_mutex_unlock(&da_q_mu); + return 0; +} + +static void da_q_drain(void) +{ + pthread_mutex_lock(&da_q_mu); + while (!da_q_is_empty_locked()) { + free(da_q[da_q_tail].data); + da_q[da_q_tail].data = NULL; + da_q_tail = (da_q_tail + 1) % DA_Q_CAP; + } + da_q_head = da_q_tail = 0; + pthread_cond_broadcast(&da_q_nonfull); + pthread_mutex_unlock(&da_q_mu); +} + +/* Consumer thread: pop item, run drift correction, writei, free. + * Mirrors dream_video.c dv_consumer_main: producer (FFMPEGThread) stays + * free to call av_read_frame so ALSA doesn't drain into underrun. */ +static void *da_consumer_main(void *arg) +{ + (void)arg; + char tn[16] = "da_consumer"; + prctl(PR_SET_NAME, (unsigned long)tn, 0, 0, 0); + + while (!da_q_stop) { + da_qitem_t item; + if (da_q_pop(&item) < 0) break; + + pthread_mutex_lock(&da_mutex); + + if (!da_handle || da_paused) { + pthread_mutex_unlock(&da_mutex); + free(item.data); + continue; + } + + unsigned int rate = item.is_passthrough ? item.pt_rate : item.rate; + unsigned int channels = item.is_passthrough ? item.pt_ch : item.channels; + if (da_alsa_setparams(rate, channels) < 0) { + pthread_mutex_unlock(&da_mutex); + free(item.data); + continue; + } + + /* PTS → tsync pts_audio (apts_speaker = chunk_pts - snd_pcm_delay). + * Same as the in-line path was doing in the producer before. */ + int64_t apts_speaker = -1; + int64_t alsa_delay_pts = 0; + if (item.pts_90k >= 0) { + da_current_pts = (unsigned long long)item.pts_90k; + snd_pcm_sframes_t df = 0; + if (rate && snd_pcm_delay(da_handle, &df) == 0 && df > 0) + alsa_delay_pts = (int64_t)df * 90000LL / (int64_t)rate; + apts_speaker = item.pts_90k - alsa_delay_pts; + if (apts_speaker >= 0) + da_tsync_checkin_apts((uint32_t)apts_speaker); + } + + if (item.is_passthrough) { + (void)da_alsa_write_raw(item.data, item.size); + pthread_mutex_unlock(&da_mutex); + free(item.data); + continue; + } + + /* ----- PCM: drift correction (anchor + mini-skip + re-arm) ----- */ + const uint8_t *write_data = item.data; + size_t write_len = item.size; + const size_t fb = (size_t)channels * sizeof(int16_t); + + if (da_skip_bytes_remaining > 0) { + size_t drop = da_skip_bytes_remaining > write_len ? write_len : da_skip_bytes_remaining; + if (fb) drop -= (drop % fb); + if (drop > 0) { + da_skip_bytes_remaining -= drop; + write_data += drop; + write_len -= drop; + } + } + + if (da_anchor_armed && apts_speaker >= 0 && rate && fb) { + int64_t pts_v = da_read_pts_video(); + if (pts_v >= 0) { + int64_t now_ms_hg = da_monotonic_ms(); + if (pts_v != da_last_seen_vpts) { + da_last_seen_vpts = pts_v; + da_last_vpts_change_ms = now_ms_hg; + } + int vpts_frozen = (da_last_vpts_change_ms != 0 + && now_ms_hg - da_last_vpts_change_ms > DA_VPTS_FROZEN_MS); + + int32_t lead_ms = (int32_t)((uint32_t)apts_speaker - (uint32_t)pts_v) / 90; + DA_DBG("anchor: lead=%+dms vpts=%lx apts=%lx%s", + lead_ms, (long)pts_v, (long)apts_speaker, + vpts_frozen ? " [vpts frozen, skip]" : ""); + if (vpts_frozen) { + /* Video decoder stalled (DASH segment underrun, etc). + * Skip destructive actions, let audio play through. + * anchor stays armed so we re-evaluate when vpts moves. */ + } else if ((lead_ms > 2000 || lead_ms < -2000) + && now_ms_hg - da_last_huge_gap_ms > 1000) + { + da_last_huge_gap_ms = now_ms_hg; + snd_pcm_drop(da_handle); + snd_pcm_prepare(da_handle); + DA_DBG("anchor: HUGE gap %+dms → ALSA flush, drop buffer (1s throttle)", lead_ms); + pthread_mutex_unlock(&da_mutex); + free(item.data); + continue; + } else if (lead_ms > 50) { + int adj = lead_ms > 2000 ? 2000 : lead_ms; + da_push_silence_ms(adj); + DA_DBG("anchor: pushed %dms silence (was %+dms ahead)", adj, lead_ms); + da_anchor_armed = 0; + } else if (lead_ms < -50) { + int adj = lead_ms < -3000 ? -3000 : lead_ms; + da_skip_bytes_remaining = + (size_t)((int64_t)(-adj) * (int64_t)rate / 1000) * fb; + DA_DBG("anchor: queued %dms skip (was %+dms behind)", -adj, lead_ms); + size_t drop = da_skip_bytes_remaining > write_len ? write_len : da_skip_bytes_remaining; + drop -= (drop % fb); + da_skip_bytes_remaining -= drop; + write_data += drop; + write_len -= drop; + da_anchor_armed = 0; + } else { + da_anchor_armed = 0; + } + } + } + + /* Post-anchor: sustained-lag recovery only. */ + if (!da_anchor_armed && apts_speaker >= 0 && rate && fb) { + int64_t pts_v = da_read_pts_video(); + if (pts_v >= 0) { + int32_t av_ms = (int32_t)((uint32_t)apts_speaker - (uint32_t)pts_v) / 90; + int64_t now_ms = da_monotonic_ms(); + if (pts_v != da_last_seen_vpts) { + da_last_seen_vpts = pts_v; + da_last_vpts_change_ms = now_ms; + } + int vpts_frozen = (da_last_vpts_change_ms != 0 + && now_ms - da_last_vpts_change_ms > DA_VPTS_FROZEN_MS); + + /* 30s heartbeat. */ + if (now_ms - da_last_sync_log_ms > 30000) { + pthread_mutex_lock(&da_q_mu); + int qd = da_q_depth_locked(); + pthread_mutex_unlock(&da_q_mu); + DA_DBG("vpts=%lx apts=%lx av=%+dms hw_delay=%lldms q=%d/%d%s [heartbeat]", + (long)pts_v, (long)apts_speaker, av_ms, + (long long)(alsa_delay_pts / 90), qd, DA_Q_CAP - 1, + vpts_frozen ? " VFROZEN" : ""); + da_last_sync_log_ms = now_ms; + } + + /* Re-arm anchor when |av| > 1000ms held >=2s; 5s cooldown. + * Skipped while vpts is frozen — destructive re-arm would + * just flush audio repeatedly. */ + int32_t abs_av = av_ms < 0 ? -av_ms : av_ms; + if (vpts_frozen) { + da_drift_outside_since_ms = 0; + } else if (abs_av > 1000) { + if (da_drift_outside_since_ms == 0) + da_drift_outside_since_ms = now_ms; + } else { + da_drift_outside_since_ms = 0; + } + if (da_drift_outside_since_ms != 0 + && now_ms - da_drift_outside_since_ms >= 2000 + && now_ms - da_last_reanchor_ms > 5000) + { + da_last_reanchor_ms = now_ms; + da_drift_outside_since_ms = 0; + da_anchor_armed = 1; + DA_DBG("sustained lag av=%+dms → re-arm anchor", av_ms); + } + } + } + + if (write_len > 0) (void)da_alsa_write(write_data, write_len); + + pthread_mutex_unlock(&da_mutex); + free(item.data); + } + return NULL; +} + +static void da_q_start(void) +{ + if (da_q_running) return; + da_q_stop = 0; + da_q_head = da_q_tail = 0; + da_q_dropped = 0; + if (pthread_create(&da_q_thread, NULL, da_consumer_main, NULL) == 0) { + da_q_running = 1; + } else { + DA_DBG("pthread_create consumer failed: %s", strerror(errno)); + } +} + +static void da_q_shutdown(void) +{ + if (!da_q_running) return; + pthread_mutex_lock(&da_q_mu); + da_q_stop = 1; + pthread_cond_broadcast(&da_q_nonemp); + pthread_cond_broadcast(&da_q_nonfull); + pthread_mutex_unlock(&da_q_mu); + pthread_join(da_q_thread, NULL); + da_q_running = 0; + da_q_drain(); +} + +/* Producer: validate input, copy payload, enqueue. The consumer thread + * does setparams + drift correction + snd_pcm_writei so that FFMPEGThread + * never blocks on ALSA. */ static int DreamAudioWrite(void *_context, void *_out) { - Context_t *context = (Context_t *)_context; - AudioVideoOut_t *out = (AudioVideoOut_t *)_out; + (void)_context; + AudioVideoOut_t *out = (AudioVideoOut_t *)_out; if (!out || !out->data || out->len == 0) return cERR_DREAMAUDIO_NO_ERROR; if (out->type && strcmp(out->type, "audio") != 0) return cERR_DREAMAUDIO_NO_ERROR; + if (!da_q_running) return cERR_DREAMAUDIO_NO_ERROR; + + int64_t pts_90k = (out->pts != INVALID_PTS_VALUE) ? (int64_t)out->pts : -1; + + /* ----- Passthrough: build IEC61937 burst, copy, enqueue ----- */ + if (da_pt_codec) { + uint8_t *burst = NULL; + size_t burst_sz = 0; + int rc = -1; + unsigned int pt_rate = 48000; + unsigned int pt_ch = 2; + + if (da_pt_codec >= 4) { + da_hbr_last_pts = pts_90k >= 0 ? pts_90k : 0; + rc = da_hbr_push(out->data, (int)out->len, da_hbr_last_pts, + &burst, &burst_sz); + pt_rate = 192000; + pt_ch = 8; + } else { + switch (da_pt_codec) { + case 1: rc = da_pt_build_ac3 (out->data, (int)out->len, &burst, &burst_sz); break; + case 2: rc = da_pt_build_eac3(out->data, (int)out->len, &burst, &burst_sz); break; + case 3: rc = da_pt_build_dts (out->data, (int)out->len, &burst, &burst_sz); break; + } + } + if (rc < 0) { + DA_DBG("passthrough build failed (codec=%d len=%u)", da_pt_codec, out->len); + return cERR_DREAMAUDIO_ERROR; + } + if (burst_sz == 0) return cERR_DREAMAUDIO_NO_ERROR; /* HBR mid-accum / EAC3 partial */ - /* container_ffmpeg.c wraps decoded PCM with pcmPrivateData_t. If extralen - * doesn't match we got something we can't play (raw bypass-mode frames - * for a codec where software_decode wasn't forced on). Bail loudly. */ + /* Burst pointer is into static / muxer-owned memory (da_pt_spdif or + * da_hbr_buf). Copy before queuing so subsequent builds can reuse + * the buffer; for HBR also advance the muxer's consumed marker. */ + uint8_t *copy = (uint8_t *)malloc(burst_sz); + if (!copy) return cERR_DREAMAUDIO_ERROR; + memcpy(copy, burst, burst_sz); + if (da_pt_codec >= 4) da_hbr_consume(burst_sz); + + da_qitem_t it = {0}; + it.data = copy; + it.size = burst_sz; + it.pts_90k = pts_90k; + it.is_passthrough = da_pt_codec; + it.pt_rate = pt_rate; + it.pt_ch = pt_ch; + if (da_q_push(&it) < 0) free(copy); + return cERR_DREAMAUDIO_NO_ERROR; + } + + /* ----- PCM path: validate header, copy, enqueue ----- */ if (!out->extradata || out->extralen != sizeof(pcmPrivateData_t)) { DA_DBG("write: missing/invalid pcmPrivateData (extralen=%u expected=%zu) — " "codec wasn't forced through software decode?", @@ -330,34 +1487,17 @@ static int DreamAudioWrite(void *_context, void *_out) return cERR_DREAMAUDIO_ERROR; } - pthread_mutex_lock(&da_mutex); + uint8_t *copy = (uint8_t *)malloc(out->len); + if (!copy) return cERR_DREAMAUDIO_ERROR; + memcpy(copy, out->data, out->len); - if (!da_handle) { - if (da_alsa_open_handle() < 0) { - pthread_mutex_unlock(&da_mutex); - return cERR_DREAMAUDIO_ERROR; - } - } - if (da_alsa_setparams(rate, channels) < 0) { - pthread_mutex_unlock(&da_mutex); - return cERR_DREAMAUDIO_ERROR; - } - - /* PTS book-keeping: container_ffmpeg.c hands us 90 kHz units in - * out->pts. Update sCURRENT_PTS for OUTPUT_PTS callers; checkin to - * tsync as a diagnostic (no effect on AV sync per dream_avsync). */ - if (out->pts != INVALID_PTS_VALUE) { - da_current_pts = (unsigned long long)out->pts; - da_tsync_checkin_apts((uint32_t)(out->pts & 0xFFFFFFFFu)); - } - - int wrote = da_alsa_write(out->data, out->len); - pthread_mutex_unlock(&da_mutex); - - if (wrote < 0) { - DA_DBG("write %u bytes failed", out->len); - return cERR_DREAMAUDIO_ERROR; - } + da_qitem_t it = {0}; + it.data = copy; + it.size = out->len; + it.pts_90k = pts_90k; + it.rate = rate; + it.channels = channels; + if (da_q_push(&it) < 0) free(copy); return cERR_DREAMAUDIO_NO_ERROR; } diff --git a/output/dream_video.c b/output/dream_video.c index 770447a..4ebf546 100644 --- a/output/dream_video.c +++ b/output/dream_video.c @@ -1,18 +1,8 @@ /* - * DreamNextGen (AMLogic dreamone/dreamtwo) video output via the AML - * /dev/amstream_vbuf port. Replaces the linuxdvb_mipsel video path - * for boxes whose /dev/dvb/adapter0/video0 shim doesn't expose - * AMSTREAM_IOC_PORT_INIT and silently drops every write (amstream - * Video buffer stays Unalloc / wcnt=0, decoder hangs in CONNECTED). - * - * Init sequence mirrors libamcodec esplayer.c + Kodi AMLCodec.cpp: - * VFORMAT → VID → SYSINFO → PORT_INIT, then write Annex-B ES - * together with TSTAMP for the PTS. - * - * See codesnake/libamcodec/examples/esplayer.c for the canonical - * userspace flow and quarnster/boxeebox-xbmc AMLCodec.cpp for the - * dec_sysinfo field semantics (format=VIDEO_DEC_FORMAT_*, not - * VFORMAT_*, and param = EXTERNAL_PTS | SYNC_OUTSIDE). + * DreamNextGen (AMLogic dreamone/dreamtwo) video output via the + * Dreambox-private DVB-API on /dev/dvb/adapter0/video0: + * VIDEO_SET_DEC_SYSINFO _IOWR('o', 65, struct dec_sysinfo) 48 B + * VIDEO_SET_FRAME _IOWR('o', 64, struct video_frame) 168 B */ #ifdef HAVE_CONFIG_H @@ -30,6 +20,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "common.h" #include "output.h" @@ -41,103 +37,115 @@ #define cERR_DREAMVIDEO_NO_ERROR 0 #define cERR_DREAMVIDEO_ERROR -1 -#define DV_DBG(fmt, ...) do { fprintf(stderr, "[dream_video] " fmt "\n", ##__VA_ARGS__); } while (0) - -/* AML amstream ioctls (linux-amlogic/include/linux/amlogic/amports/amstream.h - * — note: 'int' in the macro encodes the ioctl number; the kernel copies - * the struct from userspace itself, so the third arg of _IOW must literally - * be 'int' here. Using e.g. 'struct aml_dec_sysinfo' would change the size - * bits and yield a different ioctl number → ENOTTY.) */ -#define AMSTREAM_IOC_MAGIC 'S' -#define AMSTREAM_IOC_VB_SIZE _IOW(AMSTREAM_IOC_MAGIC, 0x01, int) -#define AMSTREAM_IOC_VFORMAT _IOW(AMSTREAM_IOC_MAGIC, 0x04, int) -#define AMSTREAM_IOC_VID _IOW(AMSTREAM_IOC_MAGIC, 0x06, int) -#define AMSTREAM_IOC_SYSINFO _IOW(AMSTREAM_IOC_MAGIC, 0x0a, int) -#define AMSTREAM_IOC_TSTAMP _IOW(AMSTREAM_IOC_MAGIC, 0x0e, int) -#define AMSTREAM_IOC_PORT_INIT _IO (AMSTREAM_IOC_MAGIC, 0x11) -#define AMSTREAM_IOC_VPAUSE _IOW(AMSTREAM_IOC_MAGIC, 0x17, int) - -/* vformat_t — passed to AMSTREAM_IOC_VFORMAT */ -#define AML_VFORMAT_MPEG12 0 -#define AML_VFORMAT_MPEG4 1 -#define AML_VFORMAT_H264 2 -#define AML_VFORMAT_MJPEG 3 -#define AML_VFORMAT_VC1 6 -#define AML_VFORMAT_AVS 7 -#define AML_VFORMAT_HEVC 11 - -/* vdec_type_t — packed into dec_sysinfo.format */ -#define AML_DEC_FORMAT_MPEG4_5 3 -#define AML_DEC_FORMAT_H264 4 -#define AML_DEC_FORMAT_HEVC 15 - -/* dec_sysinfo.param flag bits */ -#define AML_EXTERNAL_PTS 1 -#define AML_SYNC_OUTSIDE 2 - -struct aml_dec_sysinfo { +/* Dual-log: stderr (filtered by serviceapp) + /tmp/dream_video.log. */ +static void dv_log_emit(const char *line) +{ + static FILE *fp = NULL; + static int tried = 0; + if (!fp && !tried) { + tried = 1; + int fd = open("/tmp/dream_video.log", // NOSONAR + O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW | O_CLOEXEC, + 0600); + if (fd >= 0) { + fp = fdopen(fd, "a"); + if (!fp) close(fd); + } + if (fp) setvbuf(fp, NULL, _IOLBF, 0); + } + if (!fp) return; + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + fprintf(fp, "%02d:%02d:%02d.%03ld %s\n", + tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000000, line); +} +#define DV_DBG(fmt, ...) do { \ + char _dv_buf[512]; \ + int _dv_n = snprintf(_dv_buf, sizeof(_dv_buf), "[dream_video] " fmt, ##__VA_ARGS__); \ + fprintf(stderr, "%s\n", _dv_buf); \ + if (_dv_n > 0) dv_log_emit(_dv_buf); \ +} while (0) + +/* Dreambox-private DVB-API extensions. */ +struct dec_sysinfo { uint32_t format; uint32_t width; uint32_t height; - uint32_t rate; + uint32_t rate; /* duration per frame in 96000-tick units */ uint32_t extra; uint32_t status; uint32_t ratio; - void *param; + void *param; /* 8 byte on aarch64 -> total 48 bytes */ uint64_t ratio64; }; -static const char VBUF_DEV[] = "/dev/amstream_vbuf"; -static const char CNTL_DEV[] = "/dev/amvideo"; - -/* state */ -static pthread_mutex_t dv_mutex = PTHREAD_MUTEX_INITIALIZER; -static int dv_fd = -1; /* /dev/amstream_vbuf — ES writes */ -static int dv_cntl_fd = -1; /* /dev/amvideo — TSTAMP ioctl */ -static int dv_running = 0; -static int dv_inited = 0; -static unsigned long long dv_current_pts = 0; - -/* saved tsync state — restored on close so we leave the system the way - * the next consumer (Live-TV / GStreamer) found it. */ -static int dv_tsync_saved = 0; -static int dv_tsync_saved_enable = 0; -static int dv_tsync_saved_mode = 0; - -/* BCM streamtype (from exteplayer3's bcm_ioctls.h) → AML pair. */ -static int aml_format_for(int bcm_streamtype, uint32_t *vformat, uint32_t *dec_format) -{ - switch (bcm_streamtype) { - case STREAMTYPE_MPEG2: *vformat = AML_VFORMAT_MPEG12; *dec_format = 0; return 0; - case STREAMTYPE_MPEG1: *vformat = AML_VFORMAT_MPEG12; *dec_format = 0; return 0; - case STREAMTYPE_MPEG4_Part2: *vformat = AML_VFORMAT_MPEG4; *dec_format = AML_DEC_FORMAT_MPEG4_5; return 0; - case STREAMTYPE_MPEG4_H264: *vformat = AML_VFORMAT_H264; *dec_format = AML_DEC_FORMAT_H264; return 0; - case STREAMTYPE_MPEG4_H265: *vformat = AML_VFORMAT_HEVC; *dec_format = AML_DEC_FORMAT_HEVC; return 0; - case STREAMTYPE_VC1: *vformat = AML_VFORMAT_VC1; *dec_format = 0; return 0; - case STREAMTYPE_MJPEG: *vformat = AML_VFORMAT_MJPEG; *dec_format = 0; return 0; - default: return -1; - } -} +struct video_frame { + uint64_t pts; /* nanoseconds, GstClockTime style */ + ssize_t bytes[8]; + const uint8_t *data[8]; + int is_phys_addr[8]; +}; -/* AMlogic display sink blanks frames while disable_video != 0. The - * value persists across processes (enigma2 / live-TV / Standby leave it - * at 1 or 2). Re-enable on open. */ -static void dv_enable_display(void) +#define VIDEO_SET_DEC_SYSINFO _IOWR('o', 65, struct dec_sysinfo) +#define VIDEO_SET_FRAME _IOWR('o', 64, struct video_frame) + +/* vdec_type_t — packed into dec_sysinfo.format. */ +#define DEC_FORMAT_MPEG4_5 3 +#define DEC_FORMAT_H264 4 +#define DEC_FORMAT_HEVC 15 + +/* dec_sysinfo.param flag bits (kept as void *). */ +#define EXTERNAL_PTS 1 +#define SYNC_OUTSIDE 2 + +/* Dream-mapped vstream_type_t for VIDEO_SET_STREAMTYPE (only H.264=1 + * verified; MPEG2 / HEVC are best guesses). */ +#define DREAM_STREAMTYPE_MPEG2 0 +#define DREAM_STREAMTYPE_H264 1 +#define DREAM_STREAMTYPE_HEVC 8 + +static const char VIDEO_DEV[] = "/dev/dvb/adapter0/video0"; +static const char AMPOLL_DEV[] = "/dev/amvideo_poll"; + +static pthread_mutex_t dv_mutex = PTHREAD_MUTEX_INITIALIZER; +static int dv_fd = -1; +static int dv_poll_fd = -1; +static int dv_inited = 0; +static int dv_playing = 0; +static unsigned long long dv_current_pts = 0; +static uint64_t dv_frame_index = 0; +static uint64_t dv_frame_dur_ns = 20000000ULL; /* 50 fps */ + +/* Producer/consumer queue between av_read_frame and VIDEO_SET_FRAME. */ +#define DV_Q_CAP 256 /* ~5 s @ 50 fps */ +typedef struct { + uint8_t *data; + size_t size; + int64_t pts_90k; /* -1 = unknown */ +} dv_qitem_t; + +static dv_qitem_t dv_q[DV_Q_CAP]; +static int dv_q_head = 0; /* producer writes here */ +static int dv_q_tail = 0; /* consumer reads here */ +static pthread_mutex_t dv_q_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t dv_q_nonemp = PTHREAD_COND_INITIALIZER; +static pthread_cond_t dv_q_nonfull = PTHREAD_COND_INITIALIZER; +static pthread_t dv_q_thread; +static int dv_q_running = 0; +static int dv_q_stop = 0; + +/* sysfs helpers — best-effort, errors ignored. */ +static void dv_sysfs_write(const char *path, const char *value) { - FILE *f = fopen("/sys/class/video/disable_video", "w"); + FILE *f = fopen(path, "w"); if (!f) return; - fputs("0", f); + fputs(value, f); fclose(f); } -/* Kernel tsync drives AV sync between our amstream_vbuf video PTS - * (via AMSTREAM_IOC_TSTAMP) and dream_audio's pts_audio sysfs write. - * Force on AND amaster mode — gst-plugin-dreamaudiosink leaves it - * disabled, dreamvideosink/Live-TV leave it in pcrmaster (=2) which - * waits for a transport-stream PCR HLS/file playback never delivers - * (= video stutters). Save & restore the previous state so the next - * consumer (Live-TV / GStreamer) finds the system it expects. */ -static int dv_read_sysfs_int(const char *path, int *out) +static int dv_sysfs_read_int(const char *path, int *out) { FILE *f = fopen(path, "r"); if (!f) return -1; @@ -149,31 +157,265 @@ static int dv_read_sysfs_int(const char *path, int *out) return 0; } -static void dv_write_sysfs_int(const char *path, int v) +/* dreamvideosink fasst freerun_mode / show_first_frame_nosync nicht an + * (runtime strace bestätigt). Wir auch nicht. disable_video=0 reicht + * damit das video plane sichtbar wird. */ +static void dv_enable_display(void) { - FILE *f = fopen(path, "w"); - if (!f) return; - fprintf(f, "%d", v); - fclose(f); + dv_sysfs_write("/sys/class/video/disable_video", "0"); +} + +static void dv_restore_display(void) +{ + /* no-op */ +} + +/* keep signal-handler stub for forward compat — no async sysfs restore needed */ +static void dv_install_signal_handlers(void) { } + + +static int dv_streamtype_for(int bcm_streamtype, int *out) +{ + switch (bcm_streamtype) { + case STREAMTYPE_MPEG4_H264: *out = DREAM_STREAMTYPE_H264; return 0; + case STREAMTYPE_MPEG2: *out = DREAM_STREAMTYPE_MPEG2; return 0; + case STREAMTYPE_MPEG1: *out = DREAM_STREAMTYPE_MPEG2; return 0; + case STREAMTYPE_MPEG4_H265: *out = DREAM_STREAMTYPE_HEVC; return 0; + default: return -1; + } +} + +static int dv_decformat_for(int bcm_streamtype, uint32_t *out) +{ + switch (bcm_streamtype) { + case STREAMTYPE_MPEG4_H264: *out = DEC_FORMAT_H264; return 0; + case STREAMTYPE_MPEG4_H265: *out = DEC_FORMAT_HEVC; return 0; + case STREAMTYPE_MPEG2: *out = 0; return 0; + case STREAMTYPE_MPEG1: *out = 0; return 0; + default: return -1; + } +} + +/* ----- Queue helpers + consumer thread -------------------------------- */ + +static int dv_q_is_full_locked(void) { return ((dv_q_head + 1) % DV_Q_CAP) == dv_q_tail; } +static int dv_q_is_empty_locked(void) { return dv_q_head == dv_q_tail; } + +static int dv_q_depth_locked(void) +{ + return (dv_q_head - dv_q_tail + DV_Q_CAP) % DV_Q_CAP; +} + +static int dv_q_push(uint8_t *data, size_t size, int64_t pts_90k) +{ + pthread_mutex_lock(&dv_q_mu); + /* Bounded wait: 500 ms max, then drop rather than hang. */ + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += 500000000L; + if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; } + while (dv_q_is_full_locked() && !dv_q_stop) { + if (pthread_cond_timedwait(&dv_q_nonfull, &dv_q_mu, &deadline) != 0) break; + } + if (dv_q_is_full_locked() || dv_q_stop) { + pthread_mutex_unlock(&dv_q_mu); + free(data); + return -1; + } + dv_q[dv_q_head].data = data; + dv_q[dv_q_head].size = size; + dv_q[dv_q_head].pts_90k = pts_90k; + dv_q_head = (dv_q_head + 1) % DV_Q_CAP; + pthread_cond_signal(&dv_q_nonemp); + pthread_mutex_unlock(&dv_q_mu); + return 0; +} + +static int dv_q_pop(uint8_t **data_out, size_t *size_out, int64_t *pts_out) +{ + pthread_mutex_lock(&dv_q_mu); + while (dv_q_is_empty_locked() && !dv_q_stop) { + pthread_cond_wait(&dv_q_nonemp, &dv_q_mu); + } + if (dv_q_stop && dv_q_is_empty_locked()) { + pthread_mutex_unlock(&dv_q_mu); + return -1; + } + *data_out = dv_q[dv_q_tail].data; + *size_out = dv_q[dv_q_tail].size; + *pts_out = dv_q[dv_q_tail].pts_90k; + dv_q_tail = (dv_q_tail + 1) % DV_Q_CAP; + pthread_cond_signal(&dv_q_nonfull); + pthread_mutex_unlock(&dv_q_mu); + return 0; +} + +static void dv_q_drain(void) +{ + pthread_mutex_lock(&dv_q_mu); + while (!dv_q_is_empty_locked()) { + free(dv_q[dv_q_tail].data); + dv_q[dv_q_tail].data = NULL; + dv_q_tail = (dv_q_tail + 1) % DV_Q_CAP; + } + dv_q_head = dv_q_tail = 0; + pthread_mutex_unlock(&dv_q_mu); +} + +/* Periodic stats — sampled by dv_consumer_main. */ +static uint64_t dv_st_submitted = 0; /* SET_FRAME calls that returned ok */ +static uint64_t dv_st_dropped = 0; /* SET_FRAME calls that gave up after 250ms */ +static uint64_t dv_st_eagain_pollin = 0; +static uint64_t dv_st_last_log_ns = 0; +static uint64_t dv_st_last_submitted = 0; +static uint64_t dv_st_last_dropped = 0; + +/* dreamvideosink (runtime strace): + * - öffnet video0 mit O_NONBLOCK + * - öffnet /dev/amvideo_poll als O_RDWR (hat es offen, polled aber selten) + * - macht VIDEO_GET_EVENT drain initial + * - poll't VIDEO_GET_PTS regelmäßig zwischen SET_FRAMEs + * - bei EAGAIN: wartet auf amvideo_poll. */ +static int dv_submit_frame(int fd, const uint8_t *data, size_t size, int64_t pts_90k) +{ + struct video_frame fr; + memset(&fr, 0, sizeof(fr)); + fr.pts = (pts_90k > 0) ? (uint64_t)pts_90k : 0; + fr.bytes[0] = (ssize_t)size; + fr.data[0] = data; + fr.is_phys_addr[0] = 0; + + /* Drain a pending event so the kernel buffer-free path can fire. */ + static int dv_event_buf[8]; + (void)ioctl(fd, VIDEO_GET_EVENT, dv_event_buf); + + struct pollfd pfds[2]; + pfds[0].fd = fd; pfds[0].events = POLLIN; + pfds[1].fd = dv_poll_fd; pfds[1].events = POLLIN; + int nfds = (dv_poll_fd >= 0) ? 2 : 1; + + int total_wait_ms = 0; + for (;;) { + int rc = ioctl(fd, VIDEO_SET_FRAME, &fr); + if (rc >= 0) { + dv_st_submitted++; + /* Poll VIDEO_GET_PTS — gstplayer macht das ständig nach SET_FRAME, + * lest den kernel-side video pts aus. */ + uint64_t vp = 0; + (void)ioctl(fd, VIDEO_GET_PTS, &vp); + return 0; + } + if (errno != EAGAIN) { + DV_DBG("VIDEO_SET_FRAME (%zu bytes) failed: %s", size, strerror(errno)); + return -1; + } + int pr = poll(pfds, nfds, 50); + if (pr > 0) dv_st_eagain_pollin++; + if (pr <= 0) { + total_wait_ms += 50; + if (total_wait_ms > 250) { dv_st_dropped++; return 0; } + } + } +} + +static uint64_t dv_now_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +/* Submit frames as fast as the kernel video ringbuffer accepts them. + * Backpressure comes from dv_submit_frame's EAGAIN+poll loop. gstplayer + * runs the same pattern; sampling N360 showed vstream_cache ≈ 550 kB + * (≈ 3-4 frames ahead) for gstplayer vs ≈ 12 kB (avg 0.1 frame ahead) + * for our previous paced submission — the paced path left the kernel + * pacer one demuxer hiccup away from a drop on every frame. */ +static void *dv_consumer_main(void *arg) +{ + char tn[16] = "dv_consumer"; + (void)arg; + prctl(PR_SET_NAME, (unsigned long)tn, 0, 0, 0); + + int64_t pts_90k_mono = 0; /* fallback when stream PTS missing */ + int64_t last_stream_pts = -1; /* discontinuity tracking */ + dv_st_last_log_ns = dv_now_ns(); + while (!dv_q_stop) { + uint8_t *buf = NULL; + size_t sz = 0; + int64_t pts_container = -1; + if (dv_q_pop(&buf, &sz, &pts_container) < 0) break; + pthread_mutex_lock(&dv_q_mu); + int qd = dv_q_depth_locked(); + pthread_mutex_unlock(&dv_q_mu); + + /* Use stream PTS to match the audio path's timebase. Kernel + * tsync compares pts_video to pts_audio — if they're in + * different bases (monotonic vs stream) it sees huge fake + * drift and drops video to "catch up". On big PTS jumps + * (stitcher boundary > 5 s) signal tsync discontinuity so the + * kernel re-anchors instead of stalling. */ + int64_t fr_pts = pts_container; + if (fr_pts < 0) { + fr_pts = pts_90k_mono; + } else if (last_stream_pts >= 0) { + int64_t jump = fr_pts - last_stream_pts; + if (jump < -450000 || jump > 450000) { /* ±5 s @ 90 kHz */ + DV_DBG("stream PTS discontinuity: %lld → %lld (%+.2fs) — signalling tsync", + (long long)last_stream_pts, (long long)fr_pts, + (double)jump / 90000.0); + int fd = open("/sys/class/tsync/discontinue", O_WRONLY | O_CLOEXEC); + if (fd >= 0) { (void)write(fd, "1", 1); close(fd); } + } + } + last_stream_pts = fr_pts; + + if (dv_fd >= 0) dv_submit_frame(dv_fd, buf, sz, fr_pts); + free(buf); + pts_90k_mono += (int64_t)(dv_frame_dur_ns * 90 / 1000000); + uint64_t now = dv_now_ns(); + + if (now - dv_st_last_log_ns > 5000000000ULL) { + uint64_t s_delta = dv_st_submitted - dv_st_last_submitted; + uint64_t d_delta = dv_st_dropped - dv_st_last_dropped; + double secs = (double)(now - dv_st_last_log_ns) / 1e9; + DV_DBG("stats: submitted=%llu (+%llu, %.1f/s) dropped=%llu (+%llu, %.1f/s) qdepth=%d frame_dur=%llums pts_inc=%lld", + (unsigned long long)dv_st_submitted, (unsigned long long)s_delta, s_delta/secs, + (unsigned long long)dv_st_dropped, (unsigned long long)d_delta, d_delta/secs, + qd, + (unsigned long long)(dv_frame_dur_ns/1000000ULL), + (long long)(dv_frame_dur_ns * 90LL / 1000000LL)); + dv_st_last_submitted = dv_st_submitted; + dv_st_last_dropped = dv_st_dropped; + dv_st_last_log_ns = now; + } + } + return NULL; } -static void dv_setup_tsync(void) +static void dv_q_start(void) { - if (!dv_tsync_saved) { - if (dv_read_sysfs_int("/sys/class/tsync/enable", &dv_tsync_saved_enable) == 0 && - dv_read_sysfs_int("/sys/class/tsync/mode", &dv_tsync_saved_mode) == 0) - dv_tsync_saved = 1; + if (dv_q_running) return; + dv_q_stop = 0; + dv_q_head = dv_q_tail = 0; + if (pthread_create(&dv_q_thread, NULL, dv_consumer_main, NULL) == 0) { + dv_q_running = 1; + } else { + DV_DBG("pthread_create consumer failed: %s", strerror(errno)); } - dv_write_sysfs_int("/sys/class/tsync/mode", 1); /* 1 = amaster */ - dv_write_sysfs_int("/sys/class/tsync/enable", 1); } -static void dv_restore_tsync(void) +static void dv_q_shutdown(void) { - if (!dv_tsync_saved) return; - dv_write_sysfs_int("/sys/class/tsync/mode", dv_tsync_saved_mode); - dv_write_sysfs_int("/sys/class/tsync/enable", dv_tsync_saved_enable); - dv_tsync_saved = 0; + if (!dv_q_running) return; + pthread_mutex_lock(&dv_q_mu); + dv_q_stop = 1; + pthread_cond_broadcast(&dv_q_nonemp); + pthread_cond_broadcast(&dv_q_nonfull); + pthread_mutex_unlock(&dv_q_mu); + pthread_join(dv_q_thread, NULL); + dv_q_running = 0; + dv_q_drain(); } /* ----- Output_t handlers ---------------------------------------------- */ @@ -184,28 +426,33 @@ static int DreamVideoOpen(Context_t *context, char *type) pthread_mutex_lock(&dv_mutex); if (dv_fd < 0) { - dv_fd = open(VBUF_DEV, O_WRONLY); + /* dreamvideosink öffnet video0 O_NONBLOCK + amvideo_poll O_RDWR. */ + dv_fd = open(VIDEO_DEV, O_RDWR | O_NONBLOCK); if (dv_fd < 0) { - DV_DBG("open %s failed: %s", VBUF_DEV, strerror(errno)); + DV_DBG("open %s failed: %s", VIDEO_DEV, strerror(errno)); pthread_mutex_unlock(&dv_mutex); return cERR_DREAMVIDEO_ERROR; } - DV_DBG("opened %s (fd=%d)", VBUF_DEV, dv_fd); - /* PTS check-in (AMSTREAM_IOC_TSTAMP) doesn't go through - * amstream_vbuf in libamcodec — it goes through the control - * device /dev/amvideo. Without this fd TSTAMP returns EINVAL - * and the decoder never learns the first PTS, so first_stamp - * stays 0xffffffff and the pipeline freezes. */ - dv_cntl_fd = open(CNTL_DEV, O_RDWR); - if (dv_cntl_fd < 0) - DV_DBG("open %s failed: %s", CNTL_DEV, strerror(errno)); - else - DV_DBG("opened %s (fd=%d)", CNTL_DEV, dv_cntl_fd); + dv_install_signal_handlers(); + dv_q_start(); + dv_poll_fd = open(AMPOLL_DEV, O_RDWR | O_NONBLOCK); + if (dv_poll_fd < 0) + DV_DBG("open %s failed: %s", AMPOLL_DEV, strerror(errno)); dv_enable_display(); - dv_setup_tsync(); + + /* Reset state from any previous (e.g. dreamvideosink) session. */ + (void)ioctl(dv_fd, VIDEO_STOP, (void *)(uintptr_t)0); + (void)ioctl(dv_fd, VIDEO_CLEAR_BUFFER); + + if (ioctl(dv_fd, VIDEO_SELECT_SOURCE, (void *)(uintptr_t)VIDEO_SOURCE_MEMORY) < 0) + DV_DBG("SELECT_SOURCE_MEMORY failed: %s", strerror(errno)); + if (ioctl(dv_fd, VIDEO_FREEZE) < 0) + DV_DBG("FREEZE failed: %s", strerror(errno)); } dv_inited = 0; - dv_running = 0; + dv_playing = 0; + dv_frame_index = 0; + dv_frame_dur_ns = 20000000ULL; pthread_mutex_unlock(&dv_mutex); return cERR_DREAMVIDEO_NO_ERROR; } @@ -214,18 +461,26 @@ static int DreamVideoClose(Context_t *context, char *type) { if (strcmp(type, "video") != 0) return cERR_DREAMVIDEO_NO_ERROR; + /* Shut down the consumer thread before closing the fd. Drop the + * outer mutex first so the consumer (which holds dv_q_mu) can + * make forward progress. */ + pthread_mutex_unlock(&dv_mutex); + dv_q_shutdown(); pthread_mutex_lock(&dv_mutex); if (dv_fd >= 0) { + /* Return decoder to DEMUX/stopped for the next consumer. */ + ioctl(dv_fd, VIDEO_STOP, (void *)(uintptr_t)0); + ioctl(dv_fd, VIDEO_SELECT_SOURCE, (void *)(uintptr_t)VIDEO_SOURCE_DEMUX); close(dv_fd); dv_fd = -1; - if (dv_cntl_fd >= 0) { - close(dv_cntl_fd); - dv_cntl_fd = -1; - } - dv_restore_tsync(); } + if (dv_poll_fd >= 0) { + close(dv_poll_fd); + dv_poll_fd = -1; + } + dv_restore_display(); dv_inited = 0; - dv_running = 0; + dv_playing = 0; dv_current_pts = 0; pthread_mutex_unlock(&dv_mutex); DV_DBG("closed"); @@ -234,30 +489,10 @@ static int DreamVideoClose(Context_t *context, char *type) static int DreamVideoPlay(Context_t *context, char *type) { + /* Actual VIDEO_PLAY happens after SET_DEC_SYSINFO in dv_lazy_init. */ if (strcmp(type, "video") != 0) return cERR_DREAMVIDEO_NO_ERROR; - pthread_mutex_lock(&dv_mutex); - if (dv_fd >= 0) { - char *Encoding = NULL; - context->manager->video->Command(context, MANAGER_GETENCODING, &Encoding); - Writer_t *writer = getWriter(Encoding); - if (writer) { - uint32_t vformat = 0, dec_format = 0; - if (aml_format_for(writer->caps->dvbStreamType, &vformat, &dec_format) == 0) { - if (ioctl(dv_fd, AMSTREAM_IOC_VFORMAT, vformat) < 0) - DV_DBG("VFORMAT %u failed: %s", vformat, strerror(errno)); - else - DV_DBG("VFORMAT set to %u (%s)", vformat, writer->caps->name); - /* ES mode: VID=0xffff means no stream-id filtering. */ - ioctl(dv_fd, AMSTREAM_IOC_VID, 0xffff); - } else { - DV_DBG("no AML mapping for streamtype %d (encoding %s)", - writer->caps->dvbStreamType, Encoding); - } - } - free(Encoding); - } - dv_running = 1; + dv_playing = 1; pthread_mutex_unlock(&dv_mutex); return cERR_DREAMVIDEO_NO_ERROR; } @@ -275,7 +510,7 @@ static int DreamVideoPts(Context_t *context, unsigned long long *pts) return cERR_DREAMVIDEO_NO_ERROR; } -/* ----- Write ---------------------------------------------------------- */ +/* ----- Lazy init (first packet has width/height/fps) ------------------ */ static int dv_lazy_init(Context_t *context, AudioVideoOut_t *out) { @@ -284,46 +519,93 @@ static int dv_lazy_init(Context_t *context, AudioVideoOut_t *out) char *Encoding = NULL; context->manager->video->Command(context, MANAGER_GETENCODING, &Encoding); Writer_t *writer = getWriter(Encoding); - if (!writer) { - free(Encoding); - return -1; - } + free(Encoding); + if (!writer) return -1; - uint32_t vformat = 0, dec_format = 0; - if (aml_format_for(writer->caps->dvbStreamType, &vformat, &dec_format) < 0) { - free(Encoding); + uint32_t dec_format = 0; + int streamtype = 0; + if (dv_decformat_for(writer->caps->dvbStreamType, &dec_format) < 0 || + dv_streamtype_for(writer->caps->dvbStreamType, &streamtype) < 0) { + DV_DBG("no codec mapping for streamtype %d", writer->caps->dvbStreamType); return -1; } - free(Encoding); - struct aml_dec_sysinfo si; + struct dec_sysinfo si; memset(&si, 0, sizeof(si)); si.format = dec_format; si.width = out->width; si.height = out->height; - /* rate is duration-per-frame in 96000-unit ticks, NOT fps. - * exteplayer3 reports frameRate as e.g. 50000 milli-fps. */ - if (out->frameRate > 0) + /* rate = frame duration in 96000-tick units (out->frameRate is milli-fps). */ + if (out->frameRate > 0) { si.rate = (uint32_t)((uint64_t)96000ULL * 1000U / out->frameRate); - else - si.rate = 3200; /* 30 fps fallback */ - si.param = (void *)(uintptr_t)(AML_EXTERNAL_PTS | AML_SYNC_OUTSIDE); - - if (ioctl(dv_fd, AMSTREAM_IOC_SYSINFO, &si) < 0) { - DV_DBG("SYSINFO failed: %s", strerror(errno)); + dv_frame_dur_ns = (uint64_t)1000000000ULL * 1000ULL / (uint64_t)out->frameRate; + } else { + si.rate = 3200; + dv_frame_dur_ns = 33333333ULL; /* ~30 fps fallback */ + } + /* param must stay NULL — non-zero values wedge the decoder. */ + si.param = NULL; + + DV_DBG("lazy_init: w=%u h=%u out->frameRate(milli-fps)=%u → si.rate=%u(96k/frame) dv_frame_dur=%llums dec_format=%u streamtype=%d", + out->width, out->height, out->frameRate, si.rate, + (unsigned long long)(dv_frame_dur_ns/1000000ULL), dec_format, streamtype); + if (ioctl(dv_fd, VIDEO_SET_DEC_SYSINFO, &si) < 0) { + DV_DBG("VIDEO_SET_DEC_SYSINFO failed: %s", strerror(errno)); return -1; } - if (ioctl(dv_fd, AMSTREAM_IOC_PORT_INIT) < 0) { - DV_DBG("PORT_INIT failed: %s", strerror(errno)); + if (ioctl(dv_fd, VIDEO_SET_STREAMTYPE, (void *)(uintptr_t)streamtype) < 0) { + DV_DBG("VIDEO_SET_STREAMTYPE %d failed: %s", streamtype, strerror(errno)); return -1; } - DV_DBG("inited: dec_format=%u %ux%u rate=%u param=0x%x", - si.format, si.width, si.height, si.rate, - AML_EXTERNAL_PTS | AML_SYNC_OUTSIDE); + if (ioctl(dv_fd, VIDEO_PLAY) < 0) + DV_DBG("VIDEO_PLAY failed: %s", strerror(errno)); + if (ioctl(dv_fd, VIDEO_SLOWMOTION, (void *)(uintptr_t)0) < 0) + DV_DBG("VIDEO_SLOWMOTION failed: %s", strerror(errno)); + if (ioctl(dv_fd, VIDEO_FAST_FORWARD, (void *)(uintptr_t)0) < 0) + DV_DBG("VIDEO_FAST_FORWARD failed: %s", strerror(errno)); + if (ioctl(dv_fd, VIDEO_CONTINUE, (void *)(uintptr_t)5) < 0) + DV_DBG("VIDEO_CONTINUE failed: %s", strerror(errno)); + dv_inited = 1; return 0; } +/* ----- WriteV hook: hand iovec off to the consumer thread ------------- */ + +static ssize_t dv_set_frame_writev(int fd, const struct iovec *iov, int iovcnt) +{ + (void)fd; + (void)dv_frame_index; + (void)dv_frame_dur_ns; + + /* Coalesce iovec and hand off to the consumer thread. */ + size_t total = 0; + for (int i = 0; i < iovcnt; i++) total += iov[i].iov_len; + if (total == 0) return 0; + + uint8_t *buf = malloc(total); + if (!buf) { + DV_DBG("malloc(%zu) failed: %s", total, strerror(errno)); + return -1; + } + size_t off = 0; + for (int i = 0; i < iovcnt; i++) { + if (iov[i].iov_len == 0) continue; + memcpy(buf + off, iov[i].iov_base, iov[i].iov_len); + off += iov[i].iov_len; + } + + /* Snapshot the per-call PTS DreamVideoWrite cached for this frame + * so the consumer thread can tag fr.pts properly. */ + int64_t pts_90k = (int64_t)dv_current_pts; + /* Push either succeeded (consumer owns buf) or queue full/stopping + * (dv_q_push freed buf). Report success either way. */ + (void)dv_q_push(buf, total, pts_90k); + return (ssize_t)total; +} + +/* ----- Write --------------------------------------------------------- */ + static int DreamVideoWrite(void *_context, void *_out) { Context_t *context = (Context_t *)_context; @@ -340,23 +622,19 @@ static int DreamVideoWrite(void *_context, void *_out) pthread_mutex_unlock(&dv_mutex); return cERR_DREAMVIDEO_ERROR; } + /* Keep dv_frame_dur_ns aligned with the actual source rate even when + * lazy_init didn't run (first frames sometimes arrive with w/h=0). + * Without this the consumer keeps its 50fps init pacing and the + * kernel decoder swamps the display pipeline (input_fps=50 for a + * 25fps source → 4-15 frames/sec dropped, visible stutter). */ + if (out->frameRate > 0) + dv_frame_dur_ns = (uint64_t)1000000000ULL * 1000ULL / (uint64_t)out->frameRate; - /* PTS handed to the decoder via TSTAMP (90 kHz, low 32 bits) on the - * /dev/amvideo control fd — NOT amstream_vbuf. _IOW(... int) means - * the kernel does copy_from_user, so we pass a POINTER, not the - * value (libamcodec's codec_h_control plays the same trick by - * passing 'unsigned long paramter' which on aarch64 is the same - * width as a pointer). */ - if (out->pts != (int64_t)INVALID_PTS_VALUE && out->pts >= 0 && dv_cntl_fd >= 0) { - unsigned int pts32 = (unsigned int)(out->pts & 0xFFFFFFFF); - if (ioctl(dv_cntl_fd, AMSTREAM_IOC_TSTAMP, &pts32) < 0) - DV_DBG("TSTAMP failed: %s", strerror(errno)); + /* Cached for OUTPUT_PTS; the writev hook itself always feeds the + * decoder pts=0 (see dv_submit_frame). */ + if (out->pts != (int64_t)INVALID_PTS_VALUE && out->pts >= 0) dv_current_pts = (unsigned long long)out->pts; - } - /* Build the iovec via the existing per-codec writer. With the - * raw-ES patch in h264.c (and friends), iov[0].iov_len = 0 so - * the PES header is skipped — what comes out is Annex-B NALU. */ char *Encoding = NULL; context->manager->video->Command(context, MANAGER_GETENCODING, &Encoding); Writer_t *writer = getWriter(Encoding); @@ -382,7 +660,7 @@ static int DreamVideoWrite(void *_context, void *_out) call.Height = out->height; call.InfoFlags = out->infoFlags; call.Version = 0; - call.WriteV = writev_with_retry; + call.WriteV = dv_set_frame_writev; int res = writer->writeData(&call); pthread_mutex_unlock(&dv_mutex); @@ -407,9 +685,19 @@ static int DreamVideoCommand(void *_context, OutputCmd_t cmd, void *arg) case OUTPUT_STOP: return DreamVideoStop(context, (char *)arg); case OUTPUT_PTS: return DreamVideoPts(context, (unsigned long long *)arg); case OUTPUT_FLUSH: + case OUTPUT_CLEAR: + /* Drain queue + clear kernel ring + signal tsync discontinuity. */ + pthread_mutex_lock(&dv_mutex); + dv_q_drain(); + if (dv_fd >= 0) (void)ioctl(dv_fd, VIDEO_CLEAR_BUFFER); + pthread_mutex_unlock(&dv_mutex); + { + int fd = open("/sys/class/tsync/discontinue", O_WRONLY | O_CLOEXEC); + if (fd >= 0) { (void)write(fd, "1", 1); close(fd); } + } + return cERR_DREAMVIDEO_NO_ERROR; case OUTPUT_PAUSE: case OUTPUT_CONTINUE: - case OUTPUT_CLEAR: case OUTPUT_SWITCH: case OUTPUT_AVSYNC: case OUTPUT_SLOWMOTION: diff --git a/output/writer/mipsel/h264.c b/output/writer/mipsel/h264.c index afd0660..acfb149 100755 --- a/output/writer/mipsel/h264.c +++ b/output/writer/mipsel/h264.c @@ -23,6 +23,10 @@ /* Includes */ /* ***************************** */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + #include #include #include