Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions obs-plugin/src/ios-camera-source.c
Original file line number Diff line number Diff line change
Expand Up @@ -891,22 +891,40 @@ static const char *paused_suffix(struct ios_camera_source *s)
static void output_paused_still(struct ios_camera_source *s,
struct client_state *c)
{
if (!c->decoder || g_gpu_pipeline_mode)
/* Every way out of here is logged. A still that silently doesn't
* appear is indistinguishable from a frozen frame — which is how
* the first version of this shipped broken. */
if (!c->decoder || g_gpu_pipeline_mode) {
blog(LOG_INFO,
"[lenslink] paused still skipped: %s",
g_gpu_pipeline_mode ? "GPU pipeline keeps frames in "
"textures"
: "no decoder");
return;
}

int width = 0, height = 0;
if (!h264_decoder_last_frame(c->decoder, &width, &height, NULL))
if (!h264_decoder_last_frame(c->decoder, &width, &height, NULL)) {
blog(LOG_INFO,
"[lenslink] paused still skipped: nothing decoded yet");
return;
}

uint8_t thumb[LENSLINK_THUMB_W * LENSLINK_THUMB_H];
if (!h264_decoder_thumbnail(c->decoder, thumb))
if (!h264_decoder_thumbnail(c->decoder, thumb)) {
blog(LOG_INFO,
"[lenslink] paused still skipped: no thumbnail");
return;
}

/* One frame interval past the last real one keeps the still ahead
* of what OBS has already shown, on the stream's own clock. */
uint64_t pts = h264_decoder_last_pts(c->decoder);
lenslink_paused_still_output(s->source, thumb, width, height,
pts ? pts + 16666667ULL : 0);
bool drawn = lenslink_paused_still_output(s->source, thumb, width,
height,
pts ? pts + 16666667ULL : 0);
blog(LOG_INFO, "[lenslink] paused still %s (%dx%d)",
drawn ? "sent" : "failed", width, height);
}

static const char *green_screen_suffix(struct ios_camera_source *s)
Expand Down Expand Up @@ -1775,6 +1793,33 @@ static void extract_json_string(const char *json, const char *key, char *out,
* inside JSON string content (an embedded quote is always escaped), so
* this anchors to real keys only. JSONSerialization emits compact
* "key":value with no space before the colon. */
/* Same as extract_json_bool, but bounded by an explicit length so it can
* read the packet payload itself rather than the NUL-terminated copy kept
* for /api/state. That copy is capped, and a snapshot longer than the cap
* loses whatever sits past it — which is how the "paused" flag could go
* missing while everything else about the stream looked fine. */
static bool extract_json_bool_n(const char *json, size_t len, const char *key)
{
char pattern[64];
int written = snprintf(pattern, sizeof(pattern), "\"%s\":", key);
if (written <= 0)
return false;

size_t plen = (size_t)written;
if (len < plen)
return false;

for (size_t i = 0; i + plen <= len; i++) {
if (memcmp(json + i, pattern, plen) != 0)
continue;
size_t j = i + plen;
while (j < len && (json[j] == ' ' || json[j] == '\t'))
j++;
return len - j >= 4 && memcmp(json + j, "true", 4) == 0;
}
return false;
}

static bool extract_json_bool(const char *json, const char *key)
{
char pattern[64];
Expand Down Expand Up @@ -2135,20 +2180,32 @@ static bool handle_packet(struct ios_camera_source *s, struct client_state *c,
: sizeof(s->device_state) - 1;
memcpy(s->device_state, payload, n);
s->device_state[n] = 0;
bool truncated = n < hdr->payload_size;
/* Parsed from the payload, not the copy above: the copy is
* capped at sizeof(device_state) and a longer snapshot loses
* its tail. */
const char *raw = (const char *)payload;
size_t raw_len = hdr->payload_size;
bool green_screen =
extract_json_bool(s->device_state, "greenScreen");
extract_json_bool_n(raw, raw_len, "greenScreen");
/* Held rather than ended: the phone keeps the connection but
* stops sending video, so the absence of frames is a state to
* show, not a stall to diagnose. */
bool paused = extract_json_bool(s->device_state, "paused");
bool paused = extract_json_bool_n(raw, raw_len, "paused");
/* Green screen is SDR-only; the snapshot carries "hdr" (HLG)
* or "color" (Apple Log) only while a 10-bit pipeline runs.
* The app enforces the exclusivity — this is belt and braces
* so a keyed filter is never added over a stream whose green
* isn't chroma green. */
bool ten_bit = extract_json_bool(s->device_state, "hdr") ||
bool ten_bit = extract_json_bool_n(raw, raw_len, "hdr") ||
strstr(s->device_state, "\"color\":") != NULL;
pthread_mutex_unlock(&s->status_mutex);
if (truncated)
blog(LOG_WARNING,
"[lenslink] device state truncated at %zu bytes "
"(payload %u) — /api/state will be incomplete",
sizeof(s->device_state) - 1,
(unsigned)hdr->payload_size);
bool paused_changed = s->stream_paused != paused;
s->stream_paused = paused;
s->green_screen = green_screen && !s->is_screen_source;
Expand Down
11 changes: 11 additions & 0 deletions obs-plugin/src/paused-still.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "paused-still.h"

#include <obs-module.h>
#include <media-io/video-io.h>
#include <util/bmem.h>
#include <util/platform.h>

Expand Down Expand Up @@ -137,6 +138,16 @@ bool lenslink_paused_still_output(obs_source_t *source, const uint8_t *thumb,
/* Y800 as written here is full-range grey; without this OBS
* stretches 16-235 and the still comes out crushed. */
frame.full_range = true;
frame.trc = (uint8_t)VIDEO_TRC_DEFAULT;
/* The conversion matrix is the source's job, not libobs's: a frame
* handed over with the struct's zeroed matrix converts to black.
* The decoder fills this per frame (avframe_to_obs) and the still
* has to do the same or it never appears — which is exactly how it
* failed the first time. */
video_format_get_parameters_for_format(VIDEO_CS_601, VIDEO_RANGE_FULL,
frame.format, frame.color_matrix,
frame.color_range_min,
frame.color_range_max);
frame.timestamp = timestamp ? timestamp : os_gettime_ns();

/* obs_source_output_video copies into its own frame cache, so the
Expand Down