diff --git a/CHANGELOG.ceralive.md b/CHANGELOG.ceralive.md index 2958482..57e02a6 100644 --- a/CHANGELOG.ceralive.md +++ b/CHANGELOG.ceralive.md @@ -13,6 +13,132 @@ the upstream history, see `changelog.txt`. - **License:** BSD-3-Clause, preserved verbatim (`LICENSE.txt`). CeraLive additions are also BSD-3-Clause. +## ceralive-v0.0.7.3 + +Hardening release. Audited, individually-verified backports from upstream +`libuvc` PRs and the `pupil-labs` / `saki4510t` forks, each landed as a single, +fork-style-adapted commit on `hardening/v0.0.7.3` (branched from `eae7f49`, tag +`ceralive-v0.0.7.2`). Every change is either off-by-default or a pure robustness +guard — no negotiated default changes and byte-identical streaming behavior for +currently-working devices. Each entry cites its **fork-commit SHA** and upstream +provenance. Backlog IDs (A1–A14) refer to the equivalence audit in the plugin +repo's `.omo/evidence/task-1-uvc-camera-compat-stability.md`. + +### Added + +- **Runtime-configurable USB transfer-buffer count** (A2 — fork `001e8d3`; + upstream PR #291 `7620d2f`, `a100ee7`). New public API + `uvc_set_transfer_buffers(uvc_device_handle_t *devh, uint8_t count)` + (`include/libuvc/libuvc.h`, implemented in `src/stream.c`). The count is stored + on the device handle and latched by the next `uvc_start_streaming()` / + `uvc_stream_start()`, sizing the transfer allocation/submit loops. `count == 0` + (the default, unset state) preserves the prior byte-identical behavior of + allocating `LIBUVC_NUM_TRANSFER_BUFS` (100) buffers; a non-zero value is clamped + to `[2, 100]`. Setting the count while a stream on the handle is already running + is rejected with `UVC_ERROR_BUSY`. The API is pinned device-handle-level rather + than the PR's global `uvc_stream_set_default_number_of_transport_buffers()` + setter, and the static transfer arrays are retained (only the loop bounds are + made configurable). No SONAME change; the existing `uvc_start_streaming()` + signature is unchanged. + +### Fixed + +- **Retry USB alt-setting once on transient failure** (A1 — fork `3195bbc`; + upstream PR #293 `212b85d`). `uvc_stream_start()` now retries + `libusb_set_interface_alt_setting()` (bounded loop, UVC_DEBUG-logged) before + failing, so a stream start that loses a single alt-setting negotiation to a + transient USB error recovers instead of aborting. A persistently failing call + still propagates the original libusb error via the pre-existing `goto fail`. + +- **Free frame-metadata buffer in `uvc_stream_close()`** (A3 — fork `3195bbc`; + upstream PR #295 `ca65b0b`). The per-frame metadata buffer + (`strmh->frame.metadata` / `meta_outbuf`) was never freed on stream teardown, + leaking on every open→start→stop→close cycle. It is now freed alongside + `frame.data` in a guarded-free block. (Upstream PR #275 is the identical + duplicate; PR #295 was picked per the audit.) + +- **Fail loudly on zero submitted transfers** (A2 — fork `001e8d3`; upstream + PR #291). `uvc_stream_start()` previously treated `transfer_id >= 0` as success + in its submit-failure path, so a device that accepted *no* transfers reported + success and then delivered no frames. The test is now `transfer_id > 0`: when at + least one transfer is submitted the un-submitted remainder is freed and + streaming continues with fewer buffers, but when zero transfers are submitted + the allocated transfers are freed and `UVC_ERROR_IO` is returned. + +- **Repair degenerate frame descriptors** (A4 — fork `5df5401`; saki4510t + `328d14d`). During frame-descriptor parsing in `src/device.c`, a + `dwMaxVideoFrameBufferSize == 0` (seen on some DJI action cameras) is now + repaired to a sane fallback, and a `dwDefaultFrameInterval` that is zero or + outside `[dwMinFrameInterval, dwMaxFrameInterval]` is clamped into range; + UVC_DEBUG-logged. The guard is strict on the degenerate cases only — + already-sane descriptors are left byte-identical. + +- **Bounded wait in `uvc_stream_stop()`; no more indefinite teardown hang** + (A5 — fork `ab49e21`; pupil-labs `c534e3d`, upstream PR #59 / issue #152). The + indefinite `pthread_cond_wait` on transfer cancellation is replaced with a + bounded `pthread_cond_timedwait` (~1 s per iteration, ~5 s cap) that returns + `UVC_ERROR_TIMEOUT` if cancellations never complete, while still marking the + stream stopped so `uvc_stream_close()` can proceed. Transfers still in flight + are not freed (the d3318ae invariant is preserved); the fast-cancel path is + unchanged. + +- **Zero `GET_MAX` payload fallback** (A7 — fork `69c7da8`; upstream PR #277, + issue #276). When a device's `GET_MAX` returns + `dwMaxPayloadTransferSize == 0` during stream negotiation, the negotiated + control now falls back to the `GET_CUR`/descriptor value instead of propagating + a zero payload size. Composes with the existing `f4af02a` smaller-max-payload + handling with no double-handling. + +- **Corrupt / oversized payload guards** (A9, subsumes A8 — fork `69c7da8`; + upstream PR #184 + PR #212 + saki4510t `9e95b8a`). One coherent superset patch + in the payload/frame path: (1) bounds-check PTS/SCR reads in + `_uvc_process_payload` before dereferencing (`variable_offset + 4 <= header_len`, + else zero); (2) grow/realloc guard in `_uvc_populate_frame` to + `max(hold_bytes, height*step)` with an explicit `return` on allocation failure + (the fork's `UVC_EXIT` is a no-op in non-debug builds), zero-filling any short + tail and copying only `hold_bytes` — the DJI/compressed `step == 0` zero-size + tolerance from A4 is preserved; (3) a `frame_had_errors` flag on + `struct uvc_stream_handle` set on the payload error bit and on non-zero packet + status, suppressing delivery of the whole corrupt frame to the callback. The + three already-present guards (overflow clamp, per-payload error-bit skip, + per-packet status skip) were kept, not duplicated. + +- **Preserve VC-header `dwClockFrequency`** (A12 — fork `9874f4c`; pupil-labs + `92d2f82`, `74e7a96`). `uvc_parse_vc_header()` now reads `dwClockFrequency` + into the control-interface info for the `0x0110` and `0x0150` bcdUVC cases + (previously left zeroed), and `uvc_query_stream_ctrl()` stops zeroing it during + stream-control parsing. Plumbing only — the clock is not surfaced onto frames + and there is no PTS behavior change (see the plugin's `scr-investigation.md`). + +### Not backported (audit-confirmed already-equivalent) + +The following backlog items were evaluated and deliberately **not** landed — +each is already covered by the fork's current state or is out of the fork's +scope. Verdicts are from the todo-1 equivalence audit, independently +re-verified against the branch HEAD. + +- **A6 — Composite-device control interface routing** (pupil-labs `9004351`): + the fork already routes unit control requests to the parsed VideoControl + interface number (`info->ctrl_if.bInterfaceNumber`, set from the interface + scan, not hard-coded to 0). The fork base is already at pupil-labs's + post-fix state; re-picking would be a no-op. +- **A10 — `get_device_descriptor` robustness** (upstream `e001f04`): already + present as commit `eae7f49` (its message records "+ backport e001f04"); the + `uvc_scan_control` TIS-detection guard is byte-for-byte equivalent. +- **A11 — Detach only an active kernel driver** (upstream PR #224): covered by + fork commit `2f32812`, which sets `libusb_set_auto_detach_kernel_driver(…, 1)` + (libusb detaches only when a driver is actually active) and by + `uvc_claim_if()` already tolerating the no-active-driver error codes. +- **A13 — `libusb_device` refcount leak** (saki4510t `2596242`): the + libuvc-portion hunk is comment-only for `uvc_ref_device`/`uvc_unref_device` + (already correct in the fork) and otherwise touches only the Android-JNI + `uvc_get_device_with_fd`, which does not exist in this fork. The real leak fix + lives in Android libusb, out of scope. A 1000× enumerate/free loop is + ASan/LSan-clean, confirming no pre-existing leak. +- **A14 — Double-probe workaround** (upstream issue #242): implemented in the + plugin (`gstlibuvch264src`) as an opt-in `QUIRK_DOUBLE_PROBE` vid:pid quirk + seam that ships with an empty table — not a libuvc fork change. + ## ceralive-v0.0.7.1 ### Added diff --git a/include/libuvc/libuvc.h b/include/libuvc/libuvc.h index 099b2dd..47c2dfa 100644 --- a/include/libuvc/libuvc.h +++ b/include/libuvc/libuvc.h @@ -633,6 +633,8 @@ uvc_error_t uvc_start_iso_streaming( void uvc_stop_streaming(uvc_device_handle_t *devh); +uvc_error_t uvc_set_transfer_buffers(uvc_device_handle_t *devh, uint8_t count); + uvc_error_t uvc_stream_open_ctrl(uvc_device_handle_t *devh, uvc_stream_handle_t **strmh, uvc_stream_ctrl_t *ctrl); uvc_error_t uvc_stream_ctrl(uvc_stream_handle_t *strmh, uvc_stream_ctrl_t *ctrl); uvc_error_t uvc_stream_start(uvc_stream_handle_t *strmh, diff --git a/include/libuvc/libuvc_internal.h b/include/libuvc/libuvc_internal.h index bdf42db..9a7ed66 100644 --- a/include/libuvc/libuvc_internal.h +++ b/include/libuvc/libuvc_internal.h @@ -29,6 +29,14 @@ (p)[2] = (i) >> 16; \ (p)[3] = (i) >> 24; +/* Used by the degenerate frame-descriptor repair in device.c (saki4510t 328d14d). */ +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + /** Selects the nth item in a doubly linked list. n=-1 selects the last item. */ #define DL_NTH(head, out, n) \ do { \ @@ -231,6 +239,19 @@ typedef struct uvc_device_info { #endif #endif +/* Bound uvc_stream_stop()'s wait for transfer cancellation so a wedged event + * thread (e.g. a dead/unplugged device that never completes the cancelled + * transfers) cannot hang the caller forever. Each iteration waits at most + * LIBUVC_STREAM_STOP_TIMEOUT_SECS; after LIBUVC_STREAM_STOP_TIMEOUT_ATTEMPTS + * consecutive timeouts (~5s total) uvc_stream_stop returns UVC_ERROR_TIMEOUT. + */ +#ifndef LIBUVC_STREAM_STOP_TIMEOUT_SECS +#define LIBUVC_STREAM_STOP_TIMEOUT_SECS 1 +#endif +#ifndef LIBUVC_STREAM_STOP_TIMEOUT_ATTEMPTS +#define LIBUVC_STREAM_STOP_TIMEOUT_ATTEMPTS 5 +#endif + #define LIBUVC_XFER_META_BUF_SIZE ( 4 * 1024 ) struct uvc_stream_handle { @@ -266,6 +287,10 @@ struct uvc_stream_handle { /* raw metadata buffer if available */ uint8_t *meta_outbuf, *meta_holdbuf; size_t meta_got_bytes, meta_hold_bytes; + + /* set when a payload/packet of the in-progress frame reported an error; + * a flagged frame is suppressed in _uvc_swap_buffers and never delivered */ + uint8_t frame_had_errors; }; /** Handle on an open UVC device @@ -291,6 +316,11 @@ struct uvc_device_handle { /** Whether the camera is an iSight that sends one header per frame */ uint8_t is_isight; uint32_t claimed; + /** Requested number of USB transfer buffers for the NEXT stream start. + * 0 = use the library default (LIBUVC_NUM_TRANSFER_BUFS). A non-zero value is + * clamped to [2, 100] by uvc_set_transfer_buffers() and latched at + * uvc_stream_start() time. */ + uint8_t transfer_buffer_count; }; /** Context within which we communicate with devices */ diff --git a/src/device.c b/src/device.c index 2c63b6f..5ebb49b 100644 --- a/src/device.c +++ b/src/device.c @@ -1150,8 +1150,10 @@ uvc_error_t uvc_parse_vc_header(uvc_device_t *dev, info->ctrl_if.dwClockFrequency = DW_TO_INT(block + 7); break; case 0x0110: + info->ctrl_if.dwClockFrequency = DW_TO_INT(block + 7); break; case 0x0150: + info->ctrl_if.dwClockFrequency = DW_TO_INT(block + 7); break; default: UVC_EXIT(UVC_ERROR_NOT_SUPPORTED); @@ -1493,7 +1495,57 @@ uvc_error_t uvc_parse_vs_format_mjpeg(uvc_streaming_interface_t *stream_if, } /** @internal - * @brief Parse a VideoStreaming uncompressed frame block. + * @brief Repair degenerate VideoStreaming frame descriptor fields in place. + * + * Some devices (notably DJI action cameras streaming frame-based H.264/H.265) + * ship a bogus frame descriptor: a zero dwMaxVideoFrameBufferSize (the host + * uses it to size capture buffers) and/or a dwDefaultFrameInterval that is 0 or + * outside the advertised interval range. Only these degenerate values are + * touched, so a valid descriptor is left byte-identical. + * + * Adapted from saki4510t/UVCCamera 328d14d, guarded strictly on the degenerate + * cases (upstream rewrote dwMaxVideoFrameBufferSize unconditionally). The buffer + * size mirrors the zero-frame-size fixup in uvc_query_stream_ctrl: uncompressed + * formats have a real bBitsPerPixel; compressed formats (MJPEG, frame-based) + * report 0, so fall back to w*h*2. + */ +static void uvc_repair_frame_desc(uvc_format_desc_t *format, + uvc_frame_desc_t *frame) { + uint32_t min_interval, max_interval, repaired; + + if (frame->dwMaxVideoFrameBufferSize == 0 && frame->wWidth && frame->wHeight) { + if (format->bBitsPerPixel) + frame->dwMaxVideoFrameBufferSize = + (uint32_t) format->bBitsPerPixel * frame->wWidth * frame->wHeight / 8; + else + frame->dwMaxVideoFrameBufferSize = + (uint32_t) frame->wWidth * frame->wHeight * 2; + + UVC_DEBUG("repaired dwMaxVideoFrameBufferSize 0 -> %u for %ux%u frame", + frame->dwMaxVideoFrameBufferSize, frame->wWidth, frame->wHeight); + } + + if (frame->bFrameIntervalType == 0) { + min_interval = frame->dwMinFrameInterval; + max_interval = frame->dwMaxFrameInterval; + } else { + min_interval = frame->intervals[0]; + max_interval = frame->intervals[frame->bFrameIntervalType - 1]; + } + + if (min_interval && max_interval && min_interval <= max_interval && + (frame->dwDefaultFrameInterval == 0 || + frame->dwDefaultFrameInterval < min_interval || + frame->dwDefaultFrameInterval > max_interval)) { + repaired = MIN(max_interval, MAX(min_interval, frame->dwDefaultFrameInterval)); + UVC_DEBUG("repaired dwDefaultFrameInterval %u -> %u (range [%u, %u])", + frame->dwDefaultFrameInterval, repaired, min_interval, max_interval); + frame->dwDefaultFrameInterval = repaired; + } +} + +/** @internal + * @brief Parse a VideoStreaming frame-based frame block. * @ingroup device */ uvc_error_t uvc_parse_vs_frame_frame(uvc_streaming_interface_t *stream_if, @@ -1528,16 +1580,20 @@ uvc_error_t uvc_parse_vs_frame_frame(uvc_streaming_interface_t *stream_if, frame->dwMaxFrameInterval = DW_TO_INT(&block[30]); frame->dwFrameIntervalStep = DW_TO_INT(&block[34]); } else { + uint32_t interval; frame->intervals = calloc(block[21] + 1, sizeof(frame->intervals[0])); p = &block[26]; for (i = 0; i < block[21]; ++i) { - frame->intervals[i] = DW_TO_INT(p); + interval = DW_TO_INT(p); + frame->intervals[i] = interval ? interval : 1; p += 4; } frame->intervals[block[21]] = 0; } + uvc_repair_frame_desc(format, frame); + DL_APPEND(format->frame_descs, frame); UVC_EXIT(UVC_SUCCESS); @@ -1580,16 +1636,20 @@ uvc_error_t uvc_parse_vs_frame_uncompressed(uvc_streaming_interface_t *stream_if frame->dwMaxFrameInterval = DW_TO_INT(&block[30]); frame->dwFrameIntervalStep = DW_TO_INT(&block[34]); } else { + uint32_t interval; frame->intervals = calloc(block[25] + 1, sizeof(frame->intervals[0])); p = &block[26]; for (i = 0; i < block[25]; ++i) { - frame->intervals[i] = DW_TO_INT(p); + interval = DW_TO_INT(p); + frame->intervals[i] = interval ? interval : 1; p += 4; } frame->intervals[block[25]] = 0; } + uvc_repair_frame_desc(format, frame); + DL_APPEND(format->frame_descs, frame); UVC_EXIT(UVC_SUCCESS); diff --git a/src/stream.c b/src/stream.c index a86ac6a..69e97c6 100644 --- a/src/stream.c +++ b/src/stream.c @@ -262,15 +262,17 @@ uvc_error_t uvc_query_stream_ctrl( ctrl->dwMaxPayloadTransferSize = DW_TO_INT(buf + 22); if (len == 34) { - ctrl->dwClockFrequency = DW_TO_INT ( buf + 26 ); ctrl->bmFramingInfo = buf[30]; ctrl->bPreferredVersion = buf[31]; ctrl->bMinVersion = buf[32]; ctrl->bMaxVersion = buf[33]; /** @todo support UVC 1.1 */ } - else - ctrl->dwClockFrequency = devh->info->ctrl_if.dwClockFrequency; + + /* Always use the VC-header dwClockFrequency, not the buf+26 probe response: + * a zero there must not erase the enumerated value (pupil-labs 92d2f82). */ + ctrl->dwClockFrequency = devh->info->ctrl_if.dwClockFrequency; + UVC_DEBUG("ctrl->dwClockFrequency = %d", ctrl->dwClockFrequency); /* fix up block for cameras that fail to set dwMax* */ if (ctrl->dwMaxVideoFrameSize == 0) { @@ -280,6 +282,15 @@ uvc_error_t uvc_query_stream_ctrl( ctrl->dwMaxVideoFrameSize = frame->dwMaxVideoFrameBufferSize; } } + + /* fall back to the descriptor value when GET_MAX yields a zero payload (issue #276) */ + if (ctrl->dwMaxPayloadTransferSize == 0) { + uvc_frame_desc_t *frame = uvc_find_frame_desc(devh, ctrl->bFormatIndex, ctrl->bFrameIndex); + + if (frame) { + ctrl->dwMaxPayloadTransferSize = frame->dwMaxBitRate; + } + } } return UVC_SUCCESS; @@ -680,7 +691,10 @@ void _uvc_swap_buffers(uvc_stream_handle_t *strmh) { strmh->meta_outbuf = tmp_buf; strmh->meta_hold_bytes = strmh->meta_got_bytes; - pthread_cond_broadcast(&strmh->cb_cond); + /* suppress the whole frame if any of its payloads/packets reported an error */ + if (!strmh->frame_had_errors) + pthread_cond_broadcast(&strmh->cb_cond); + strmh->frame_had_errors = 0; pthread_mutex_unlock(&strmh->cb_mutex); strmh->seq++; @@ -753,6 +767,7 @@ void _uvc_process_payload(uvc_stream_handle_t *strmh, uint8_t *payload, size_t p if (header_info & 0x40) { UVC_DEBUG("bad packet: error bit set"); + strmh->frame_had_errors = 1; return; } @@ -766,13 +781,25 @@ void _uvc_process_payload(uvc_stream_handle_t *strmh, uint8_t *payload, size_t p strmh->fid = header_info & 1; if (header_info & (1 << 2)) { - strmh->pts = DW_TO_INT(payload + variable_offset); + /* guard the 4-byte read: some cameras set the flag but truncate the header */ + if (variable_offset + 4 <= header_len) { + strmh->pts = DW_TO_INT(payload + variable_offset); + } else { + UVC_DEBUG("bogus packet: PTS flag set but header too short"); + strmh->pts = 0; + } variable_offset += 4; } if (header_info & (1 << 3)) { /** @todo read the SOF token counter */ - strmh->last_scr = DW_TO_INT(payload + variable_offset); + /* guard the 4-byte SCR read (the field is 6 bytes: 4 SCR + 2 SOF) */ + if (variable_offset + 4 <= header_len) { + strmh->last_scr = DW_TO_INT(payload + variable_offset); + } else { + UVC_DEBUG("bogus packet: SCR flag set but header too short"); + strmh->last_scr = 0; + } variable_offset += 6; } @@ -828,6 +855,7 @@ void LIBUSB_CALL _uvc_stream_callback(struct libusb_transfer *transfer) { if (pkt->status != 0) { UVC_DEBUG("bad packet (isochronous transfer); status: %d", pkt->status); + strmh->frame_had_errors = 1; continue; } @@ -1068,6 +1096,43 @@ uvc_error_t uvc_stream_open_ctrl(uvc_device_handle_t *devh, uvc_stream_handle_t return ret; } +/** @brief Override the number of USB transfer buffers used for streaming. + * @ingroup streaming + * + * The chosen count is latched by the next uvc_start_streaming()/ + * uvc_stream_start() on this device handle. More buffers absorb host-side + * scheduling jitter at the cost of RAM. + * + * @param devh UVC device handle + * @param count Desired count. 0 restores the library default + * (LIBUVC_NUM_TRANSFER_BUFS); any other value is clamped to [2, 100]. + * + * @return UVC_SUCCESS on success; UVC_ERROR_INVALID_PARAM if @p devh is NULL; + * UVC_ERROR_BUSY if a stream on @p devh is already running (the count is + * consumed only at stream start and cannot change mid-stream). + */ +uvc_error_t uvc_set_transfer_buffers(uvc_device_handle_t *devh, uint8_t count) { + uvc_stream_handle_t *strmh; + + if (devh == NULL) + return UVC_ERROR_INVALID_PARAM; + + for (strmh = devh->streams; strmh != NULL; strmh = strmh->next) { + if (strmh->running) + return UVC_ERROR_BUSY; + } + + if (count != 0) { + if (count < 2) + count = 2; + else if (count > 100) + count = 100; + } + + devh->transfer_buffer_count = count; + return UVC_SUCCESS; +} + /** Begin streaming video from the stream into the callback function. * @ingroup streaming * @@ -1094,6 +1159,7 @@ uvc_error_t uvc_stream_start( size_t total_transfer_size = 0; struct libusb_transfer *transfer; int transfer_id; + int num_transfer_bufs = LIBUVC_NUM_TRANSFER_BUFS; ctrl = &strmh->cur_ctrl; @@ -1110,6 +1176,14 @@ uvc_error_t uvc_stream_start( strmh->pts = 0; strmh->last_scr = 0; + if (strmh->devh->transfer_buffer_count != 0) { + num_transfer_bufs = strmh->devh->transfer_buffer_count; + if (num_transfer_bufs < 2) + num_transfer_bufs = 2; + if (num_transfer_bufs > LIBUVC_NUM_TRANSFER_BUFS) + num_transfer_bufs = LIBUVC_NUM_TRANSFER_BUFS; + } + frame_desc = uvc_find_frame_desc_stream(strmh, ctrl->bFormatIndex, ctrl->bFrameIndex); if (!frame_desc) { ret = UVC_ERROR_INVALID_PARAM; @@ -1145,6 +1219,8 @@ uvc_error_t uvc_stream_start( size_t endpoint_bytes_per_packet = 0; /* Index of the altsetting */ int alt_idx, ep_idx; + /* Number of attempts to select the altsetting */ + int attempts; config_bytes_per_packet = strmh->cur_ctrl.dwMaxPayloadTransferSize; @@ -1200,17 +1276,24 @@ uvc_error_t uvc_stream_start( goto fail; } - /* Select the altsetting */ - ret = libusb_set_interface_alt_setting(strmh->devh->usb_devh, - altsetting->bInterfaceNumber, - altsetting->bAlternateSetting); + /* Select the altsetting, retrying on transient failure (up to 3 attempts) */ + attempts = 0; + do { + attempts++; + ret = libusb_set_interface_alt_setting(strmh->devh->usb_devh, + altsetting->bInterfaceNumber, + altsetting->bAlternateSetting); + if (ret != UVC_SUCCESS && attempts < 3) { + UVC_DEBUG("libusb_set_interface_alt_setting failed (attempt %d of 3), retrying", attempts); + } + } while (ret != UVC_SUCCESS && attempts < 3); if (ret != UVC_SUCCESS) { UVC_DEBUG("libusb_set_interface_alt_setting failed"); goto fail; } /* Set up the transfers */ - for (transfer_id = 0; transfer_id < LIBUVC_NUM_TRANSFER_BUFS; ++transfer_id) { + for (transfer_id = 0; transfer_id < num_transfer_bufs; ++transfer_id) { transfer = libusb_alloc_transfer(packets_per_transfer); strmh->transfers[transfer_id] = transfer; strmh->transfer_bufs[transfer_id] = malloc(total_transfer_size); @@ -1223,7 +1306,7 @@ uvc_error_t uvc_stream_start( libusb_set_iso_packet_lengths(transfer, endpoint_bytes_per_packet); } } else { - for (transfer_id = 0; transfer_id < LIBUVC_NUM_TRANSFER_BUFS; + for (transfer_id = 0; transfer_id < num_transfer_bufs; ++transfer_id) { transfer = libusb_alloc_transfer(0); strmh->transfers[transfer_id] = transfer; @@ -1247,7 +1330,7 @@ uvc_error_t uvc_stream_start( pthread_create(&strmh->cb_thread, NULL, _uvc_user_caller, (void*) strmh); } - for (transfer_id = 0; transfer_id < LIBUVC_NUM_TRANSFER_BUFS; + for (transfer_id = 0; transfer_id < num_transfer_bufs; transfer_id++) { ret = libusb_submit_transfer(strmh->transfers[transfer_id]); if (ret != UVC_SUCCESS) { @@ -1256,13 +1339,23 @@ uvc_error_t uvc_stream_start( } } - if ( ret != UVC_SUCCESS && transfer_id >= 0 ) { - for ( ; transfer_id < LIBUVC_NUM_TRANSFER_BUFS; transfer_id++) { + if ( ret != UVC_SUCCESS && transfer_id > 0 ) { + for ( ; transfer_id < num_transfer_bufs; transfer_id++) { free ( strmh->transfers[transfer_id]->buffer ); libusb_free_transfer ( strmh->transfers[transfer_id]); strmh->transfers[transfer_id] = 0; } ret = UVC_SUCCESS; + } else if ( ret != UVC_SUCCESS ) { + UVC_DEBUG("no transfers could be submitted; failing with UVC_ERROR_IO"); + for (transfer_id = 0; transfer_id < num_transfer_bufs; transfer_id++) { + if (strmh->transfers[transfer_id]) { + free ( strmh->transfers[transfer_id]->buffer ); + libusb_free_transfer ( strmh->transfers[transfer_id]); + strmh->transfers[transfer_id] = 0; + } + } + ret = UVC_ERROR_IO; } UVC_EXIT(ret); @@ -1376,12 +1469,37 @@ void _uvc_populate_frame(uvc_stream_handle_t *strmh) { frame->sequence = strmh->hold_seq; frame->capture_time_finished = strmh->capture_time_finished; - /* copy the image data from the hold buffer to the frame (unnecessary extra buf?) */ - if (frame->data_bytes < strmh->hold_bytes) { - frame->data = realloc(frame->data, strmh->hold_bytes); + /* copy the image data from the hold buffer to the frame (unnecessary extra buf?) + * Grow the buffer to hold at least what was received and, for uncompressed + * formats, zero-fill any shortfall below height*step so data_bytes never + * exceeds the allocation and the tail leaks no stale heap. For compressed / + * frame-based formats (H264/H265/MJPEG) step==0, so height*step==0 and this + * degenerates to the plain hold_bytes path -- the DJI zero-size tolerance is + * preserved and no zero-size realloc is forced. */ + if (frame->data_bytes < strmh->hold_bytes || + frame->data_bytes < frame->height * frame->step) { + if (strmh->hold_bytes >= frame->height * frame->step) { + frame->data = realloc(frame->data, strmh->hold_bytes); + if (frame->data == NULL) { + UVC_EXIT(UVC_ERROR_NO_MEM); + return; + } + frame->data_bytes = strmh->hold_bytes; + } else { + frame->data = realloc(frame->data, frame->height * frame->step); + if (frame->data == NULL) { + UVC_EXIT(UVC_ERROR_NO_MEM); + return; + } + memset(frame->data + strmh->hold_bytes, 0, + frame->height * frame->step - strmh->hold_bytes); + frame->data_bytes = frame->height * frame->step; + } + } else { + frame->data_bytes = strmh->hold_bytes; } - frame->data_bytes = strmh->hold_bytes; - memcpy(frame->data, strmh->holdbuf, frame->data_bytes); + + memcpy(frame->data, strmh->holdbuf, strmh->hold_bytes); if (strmh->meta_hold_bytes > 0) { @@ -1498,6 +1616,9 @@ void uvc_stop_streaming(uvc_device_handle_t *devh) { */ uvc_error_t uvc_stream_stop(uvc_stream_handle_t *strmh) { int i; + uvc_error_t ret = UVC_SUCCESS; + int timeout_attempts = 0; + struct timespec ts; if (!strmh->running) return UVC_ERROR_INVALID_PARAM; @@ -1522,7 +1643,36 @@ uvc_error_t uvc_stream_stop(uvc_stream_handle_t *strmh) { } if(i == LIBUVC_NUM_TRANSFER_BUFS ) break; - pthread_cond_wait(&strmh->cb_cond, &strmh->cb_mutex); + + /* Bounded wait (mirrors uvc_stream_get_frame's timedwait): a dead device's + * cancelled transfers can fail to complete, so a plain pthread_cond_wait + * here hangs the caller forever. Wait at most LIBUVC_STREAM_STOP_TIMEOUT_SECS + * per iteration; give up after LIBUVC_STREAM_STOP_TIMEOUT_ATTEMPTS timeouts. + * The stream is already marked stopped (running=0 above) so uvc_stream_close + * can proceed, and in-flight transfers are still left for _uvc_stream_callback + * to free (never freed here). */ + ts.tv_sec = 0; + ts.tv_nsec = 0; + +#if _POSIX_TIMERS > 0 + clock_gettime(CLOCK_REALTIME, &ts); +#else + { + struct timeval tv; + gettimeofday(&tv, NULL); + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; + } +#endif + + ts.tv_sec += LIBUVC_STREAM_STOP_TIMEOUT_SECS; + + if (pthread_cond_timedwait(&strmh->cb_cond, &strmh->cb_mutex, &ts) == ETIMEDOUT) { + if (++timeout_attempts >= LIBUVC_STREAM_STOP_TIMEOUT_ATTEMPTS) { + ret = UVC_ERROR_TIMEOUT; + break; + } + } } while(1); // Kick the user thread awake pthread_cond_broadcast(&strmh->cb_cond); @@ -1536,7 +1686,7 @@ uvc_error_t uvc_stream_stop(uvc_stream_handle_t *strmh) { pthread_join(strmh->cb_thread, NULL); } - return UVC_SUCCESS; + return ret; } /** @brief Close stream. @@ -1555,6 +1705,9 @@ void uvc_stream_close(uvc_stream_handle_t *strmh) { if (strmh->frame.data) free(strmh->frame.data); + if (strmh->frame.metadata) + free(strmh->frame.metadata); + free(strmh->outbuf); free(strmh->holdbuf);