Skip to content
Open
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
8 changes: 8 additions & 0 deletions internal/native/cgo/ctrl.c
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,14 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status();
}

int jetkvm_video_get_snapshot(uint8_t **out_buf, size_t *out_len) {
return video_get_snapshot(out_buf, out_len);
}

void jetkvm_video_free_snapshot(uint8_t *buf) {
video_free_snapshot(buf);
}

int jetkvm_video_init(float factor) {
return video_init(factor);
}
Expand Down
3 changes: 3 additions & 0 deletions internal/native/cgo/ctrl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define VIDEO_DAEMON_CTRL_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>

Expand Down Expand Up @@ -66,6 +67,8 @@ int jetkvm_video_set_edid(const char *edid_hex);
char *jetkvm_video_get_edid_hex();
char *jetkvm_video_log_status();
jetkvm_video_state_t *jetkvm_video_get_status();
int jetkvm_video_get_snapshot(uint8_t **out_buf, size_t *out_len);
void jetkvm_video_free_snapshot(uint8_t *buf);

void video_report_format(bool ready, const char *error, u_int16_t width, u_int16_t height, double frame_per_second);
void video_send_format_report();
Expand Down
256 changes: 256 additions & 0 deletions internal/native/cgo/video.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

int sub_dev_fd = -1;
#define VENC_CHANNEL 0
#define VENC_CHANNEL_JPEG 1 // second encoder channel, used only for on-demand snapshots
MB_POOL memPool = MB_INVALID_POOLID;

bool sleep_mode_available = false;
Expand All @@ -46,6 +47,10 @@ float quality_factor = 1.0f;
int codec_type = 0;

static void *venc_read_stream(void *arg);
static int32_t venc_jpeg_start(int32_t width, int32_t height);
static void venc_jpeg_stop(void);
bool get_streaming_flag();
bool get_streaming_stopped();

RK_U64 get_us()
{
Expand Down Expand Up @@ -288,6 +293,14 @@ static int32_t venc_start(int32_t bitrate, int32_t max_bitrate, int32_t width, i
return ret;
}

// Snapshot support is best-effort: if the JPEG channel fails to start,
// keep streaming the primary H.264/H.265 channel without it.
int32_t jpeg_ret = venc_jpeg_start(width, height);
if (jpeg_ret != RK_SUCCESS)
{
log_warn("failed to start JPEG snapshot channel: %#x", jpeg_ret);
}

venc_running = true;
venc_read_thread = malloc(sizeof(pthread_t));
if (pthread_create(venc_read_thread, NULL, venc_read_stream, NULL) != 0)
Expand All @@ -303,6 +316,8 @@ static int32_t venc_stop()
{
venc_running = false;

venc_jpeg_stop();

int32_t ret;
ret = RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL);
if (ret != RK_SUCCESS)
Expand All @@ -328,6 +343,239 @@ static int32_t venc_stop()
return RK_SUCCESS;
}

// --- On-demand JPEG snapshot channel -----------------------------------
//
// A second VENC channel, created/destroyed alongside VENC_CHANNEL so it
// always matches the current capture resolution. It receives frames
// continuously (like VENC_CHANNEL) but produces no output unless
// run_video_stream() explicitly feeds it a frame, which only happens while
// a video_get_snapshot() call is pending.

static pthread_mutex_t snapshot_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t snapshot_cond = PTHREAD_COND_INITIALIZER;
static bool venc_jpeg_running = false;
static bool snapshot_requested = false;
static bool snapshot_ready = false;
static uint8_t *snapshot_buf = NULL;
static size_t snapshot_len = 0;
static int snapshot_result = 0;

static void populate_venc_jpeg_attr(VENC_CHN_ATTR_S *stAttr, RK_U32 width, RK_U32 height)
{
memset(stAttr, 0, sizeof(VENC_CHN_ATTR_S));

stAttr->stVencAttr.enType = RK_VIDEO_ID_JPEG;
stAttr->stVencAttr.enPixelFormat = RK_FMT_YUV422_YUYV;
stAttr->stVencAttr.u32PicWidth = width;
stAttr->stVencAttr.u32PicHeight = height;
stAttr->stVencAttr.u32VirWidth = RK_ALIGN_16(width);
stAttr->stVencAttr.u32VirHeight = RK_ALIGN_16(height);
stAttr->stVencAttr.u32StreamBufCnt = 2;
stAttr->stVencAttr.u32BufSize = width * height * 3 / 2;
stAttr->stVencAttr.enMirror = MIRROR_NONE;
}

static int32_t venc_jpeg_start(int32_t width, int32_t height)
{
VENC_CHN_ATTR_S stAttr;
populate_venc_jpeg_attr(&stAttr, (RK_U32)width, (RK_U32)height);

int32_t ret = RK_MPI_VENC_CreateChn(VENC_CHANNEL_JPEG, &stAttr);
if (ret != RK_SUCCESS)
{
return ret;
}

VENC_JPEG_PARAM_S stJpegParam;
memset(&stJpegParam, 0, sizeof(stJpegParam));
stJpegParam.u32Qfactor = 90;
ret = RK_MPI_VENC_SetJpegParam(VENC_CHANNEL_JPEG, &stJpegParam);
if (ret != RK_SUCCESS)
{
log_warn("RK_MPI_VENC_SetJpegParam failed: %#x, using encoder default quality", ret);
}

VENC_RECV_PIC_PARAM_S stRecvParam;
memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
stRecvParam.s32RecvPicNum = -1;
ret = RK_MPI_VENC_StartRecvFrame(VENC_CHANNEL_JPEG, &stRecvParam);
if (ret != RK_SUCCESS)
{
RK_MPI_VENC_DestroyChn(VENC_CHANNEL_JPEG);
return ret;
}

pthread_mutex_lock(&snapshot_mutex);
venc_jpeg_running = true;
pthread_mutex_unlock(&snapshot_mutex);

return RK_SUCCESS;
}

static void venc_jpeg_stop(void)
{
pthread_mutex_lock(&snapshot_mutex);
if (!venc_jpeg_running)
{
pthread_mutex_unlock(&snapshot_mutex);
return;
}
venc_jpeg_running = false;

// Wake up a snapshot request that's still waiting; the channel is going away.
if (snapshot_requested)
{
snapshot_requested = false;
snapshot_result = VIDEO_SNAPSHOT_ERR_NOT_STREAMING;
snapshot_ready = true;
pthread_cond_broadcast(&snapshot_cond);
}
pthread_mutex_unlock(&snapshot_mutex);

RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL_JPEG);
RK_MPI_VENC_DestroyChn(VENC_CHANNEL_JPEG);
}

// Delivers a snapshot result to the (single) waiting video_get_snapshot()
// call and wakes it up. Takes ownership of buf (may be NULL on error).
static void complete_snapshot_request(uint8_t *buf, size_t len, int result)
{
pthread_mutex_lock(&snapshot_mutex);
snapshot_requested = false;
free(snapshot_buf); // defensive; should already be NULL here
snapshot_buf = buf;
snapshot_len = len;
snapshot_result = result;
snapshot_ready = true;
pthread_cond_broadcast(&snapshot_cond);
pthread_mutex_unlock(&snapshot_mutex);
}

// Runs on the video capture thread, so it must stay fast: it blocks
// VIDIOC_QBUF on the primary V4L2 capture loop (only input_buffer_count
// buffers deep) for as long as it takes. A single failed attempt here just
// means this frame's snapshot is skipped — the Go-side caller retries
// against the next captured frame rather than this function retrying
// in-place and doubling the stall.
//
// pFrame is the just-captured raw frame that was already handed to
// VENC_CHANNEL; reusing it here avoids capturing a second frame off V4L2
// just for the snapshot.
static void handle_snapshot_request(VIDEO_FRAME_INFO_S *pFrame)
{
if (RK_MPI_VENC_SendFrame(VENC_CHANNEL_JPEG, pFrame, 150) != RK_SUCCESS)
{
log_error("snapshot: RK_MPI_VENC_SendFrame(JPEG) failed");
complete_snapshot_request(NULL, 0, VIDEO_SNAPSHOT_ERR_ENCODE);
return;
}

VENC_STREAM_S stJpegStream;
memset(&stJpegStream, 0, sizeof(stJpegStream));
stJpegStream.pstPack = malloc(sizeof(VENC_PACK_S));
if (stJpegStream.pstPack == NULL)
{
complete_snapshot_request(NULL, 0, VIDEO_SNAPSHOT_ERR_NOMEM);
return;
}

int32_t ret = RK_MPI_VENC_GetStream(VENC_CHANNEL_JPEG, &stJpegStream, 150);
if (ret != RK_SUCCESS)
{
log_error("snapshot: RK_MPI_VENC_GetStream(JPEG) failed %#x", ret);
free(stJpegStream.pstPack);
complete_snapshot_request(NULL, 0, VIDEO_SNAPSHOT_ERR_ENCODE);
return;
}

void *pData = RK_MPI_MB_Handle2VirAddr(stJpegStream.pstPack->pMbBlk);
size_t len = (size_t)stJpegStream.pstPack->u32Len;
uint8_t *copy = malloc(len);
if (copy == NULL)
{
RK_MPI_VENC_ReleaseStream(VENC_CHANNEL_JPEG, &stJpegStream);
free(stJpegStream.pstPack);
complete_snapshot_request(NULL, 0, VIDEO_SNAPSHOT_ERR_NOMEM);
return;
}
memcpy(copy, pData, len);

RK_MPI_VENC_ReleaseStream(VENC_CHANNEL_JPEG, &stJpegStream);
free(stJpegStream.pstPack);

complete_snapshot_request(copy, len, 0);
}

int video_get_snapshot(uint8_t **out_buf, size_t *out_len)
{
if (!get_streaming_flag() || get_streaming_stopped())
{
return VIDEO_SNAPSHOT_ERR_NOT_STREAMING;
}

pthread_mutex_lock(&snapshot_mutex);

if (!venc_jpeg_running)
{
pthread_mutex_unlock(&snapshot_mutex);
return VIDEO_SNAPSHOT_ERR_NOT_STREAMING;
}

snapshot_requested = true;
snapshot_ready = false;
free(snapshot_buf);
snapshot_buf = NULL;
snapshot_len = 0;
snapshot_result = 0;

// This function is called under the Go side's cgoLock (a single global
// mutex shared by every native call, including UI ticks), so the wait
// here directly stalls unrelated native operations for its duration.
// Kept short; the Go-side caller (captureScreenshot) retries across
// several calls rather than this one call waiting longer.
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 300000000L; // 300ms: one frame period plus JPEG encode headroom
if (ts.tv_nsec >= 1000000000L)
{
ts.tv_sec += 1;
ts.tv_nsec -= 1000000000L;
}

int wait_rc = 0;
while (!snapshot_ready && wait_rc == 0)
{
wait_rc = pthread_cond_timedwait(&snapshot_cond, &snapshot_mutex, &ts);
}

int result;
if (!snapshot_ready)
{
snapshot_requested = false;
result = VIDEO_SNAPSHOT_ERR_TIMEOUT;
}
else if (snapshot_result != 0 || snapshot_buf == NULL)
{
result = (snapshot_result != 0) ? snapshot_result : VIDEO_SNAPSHOT_ERR_ENCODE;
}
else
{
*out_buf = snapshot_buf;
*out_len = snapshot_len;
snapshot_buf = NULL;
snapshot_len = 0;
result = 0;
}

pthread_mutex_unlock(&snapshot_mutex);
return result;
}

void video_free_snapshot(uint8_t *buf)
{
free(buf);
}

struct buffer
{
struct v4l2_plane plane_buffer;
Expand Down Expand Up @@ -751,6 +999,14 @@ void *run_video_stream(void *arg)

num++;

pthread_mutex_lock(&snapshot_mutex);
bool want_snapshot = venc_jpeg_running && snapshot_requested;
pthread_mutex_unlock(&snapshot_mutex);
if (want_snapshot)
{
handle_snapshot_request(&stFrame);
}

if (ioctl(video_dev_fd, VIDIOC_QBUF, &buf) < 0)
log_error("failure VIDIOC_QBUF: %s", strerror(errno));
}
Expand Down
24 changes: 24 additions & 0 deletions internal/native/cgo/video.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#ifndef VIDEO_DAEMON_VIDEO_H
#define VIDEO_DAEMON_VIDEO_H

#include <stddef.h>
#include <stdint.h>

/**
* @brief Initialize the video subsystem
*
Expand Down Expand Up @@ -62,4 +65,25 @@ void video_set_codec_type(int type);
*/
int video_get_codec_type();

#define VIDEO_SNAPSHOT_ERR_NOT_STREAMING (-1) // no active video stream to snapshot
#define VIDEO_SNAPSHOT_ERR_TIMEOUT (-2) // no frame captured within the deadline
#define VIDEO_SNAPSHOT_ERR_ENCODE (-3) // JPEG encoder failed
#define VIDEO_SNAPSHOT_ERR_NOMEM (-4) // failed to allocate the output buffer

/**
* @brief Capture a single JPEG-encoded snapshot of the current video frame.
*
* Blocks until the next captured frame has been JPEG-encoded, or until an
* internal deadline expires. On success, *out_buf is a malloc'd buffer of
* *out_len bytes that the caller must release with video_free_snapshot().
*
* @return 0 on success, a negative VIDEO_SNAPSHOT_ERR_* code on failure
*/
int video_get_snapshot(uint8_t **out_buf, size_t *out_len);

/**
* @brief Free a buffer returned by video_get_snapshot()
*/
void video_free_snapshot(uint8_t *buf);

#endif //VIDEO_DAEMON_VIDEO_H
19 changes: 19 additions & 0 deletions internal/native/cgo_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,25 @@ func videoGetStreamingStatus() VideoStreamingStatus {
return VideoStreamingStatus(isStreaming)
}

func videoGetSnapshot() ([]byte, error) {
cgoLock.Lock()
defer cgoLock.Unlock()

var buf *C.uint8_t
var length C.size_t

ret := C.jetkvm_video_get_snapshot(&buf, &length)
if ret != 0 {
if ret == -1 {
return nil, ErrVideoNotStreaming
}
return nil, fmt.Errorf("failed to capture video snapshot: %d", int(ret))
}
defer C.jetkvm_video_free_snapshot(buf)

return C.GoBytes(unsafe.Pointer(buf), C.int(length)), nil
}

func videoLogStatus() string {
cgoLock.Lock()
defer cgoLock.Unlock()
Expand Down
Loading
Loading