pyinsta360 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinsta360-0.1.0/CMakeLists.txt +48 -0
- pyinsta360-0.1.0/LICENSE +21 -0
- pyinsta360-0.1.0/PKG-INFO +178 -0
- pyinsta360-0.1.0/README.md +150 -0
- pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/REPORT.md +92 -0
- pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/repro.cc +20 -0
- pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/run.sh +22 -0
- pyinsta360-0.1.0/docs/sdk-bugs/stop-recording-url/REPORT.md +72 -0
- pyinsta360-0.1.0/docs/sdk-bugs/stop-recording-url/repro.cc +58 -0
- pyinsta360-0.1.0/examples/stream_smoke.py +77 -0
- pyinsta360-0.1.0/pyproject.toml +45 -0
- pyinsta360-0.1.0/python/pyinsta360/__init__.py +94 -0
- pyinsta360-0.1.0/python/pyinsta360/_guard.py +21 -0
- pyinsta360-0.1.0/python/pyinsta360/_sdk.pyi +197 -0
- pyinsta360-0.1.0/python/pyinsta360/_sdk_enums.pyi +462 -0
- pyinsta360-0.1.0/python/pyinsta360/decode.py +57 -0
- pyinsta360-0.1.0/python/pyinsta360/py.typed +0 -0
- pyinsta360-0.1.0/python/pyinsta360/supervise.py +112 -0
- pyinsta360-0.1.0/src/enums_gen.inc +452 -0
- pyinsta360-0.1.0/src/module.cc +432 -0
- pyinsta360-0.1.0/src/py_camera.cc +323 -0
- pyinsta360-0.1.0/src/py_camera.h +131 -0
- pyinsta360-0.1.0/src/stream_queues.cc +104 -0
- pyinsta360-0.1.0/src/stream_queues.h +68 -0
- pyinsta360-0.1.0/tests/conftest.py +70 -0
- pyinsta360-0.1.0/tests/fake_sdk/fake_camera_sdk.cc +439 -0
- pyinsta360-0.1.0/tests/supervise_targets.py +38 -0
- pyinsta360-0.1.0/tests/test_build.py +100 -0
- pyinsta360-0.1.0/tests/test_camera.py +572 -0
- pyinsta360-0.1.0/tests/test_decode.py +82 -0
- pyinsta360-0.1.0/tests/test_supervise.py +65 -0
- pyinsta360-0.1.0/tools/build_fake_sdk.sh +13 -0
- pyinsta360-0.1.0/tools/gen_enums.py +116 -0
- pyinsta360-0.1.0/tools/test.sh +9 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(pyinsta360 LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
7
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
8
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
9
|
+
endif()
|
|
10
|
+
|
|
11
|
+
if(NOT INSTA360_SDK_DIR AND DEFINED ENV{INSTA360_SDK_DIR})
|
|
12
|
+
set(INSTA360_SDK_DIR "$ENV{INSTA360_SDK_DIR}")
|
|
13
|
+
endif()
|
|
14
|
+
if(NOT INSTA360_SDK_DIR)
|
|
15
|
+
message(FATAL_ERROR
|
|
16
|
+
"INSTA360_SDK_DIR is not set. Point it at the extracted Insta360 CameraSDK "
|
|
17
|
+
"(the directory containing include/ and lib/libCameraSDK.so), either as an "
|
|
18
|
+
"environment variable or with -DINSTA360_SDK_DIR=... (the SDK is proprietary "
|
|
19
|
+
"and is not shipped with this repository).")
|
|
20
|
+
endif()
|
|
21
|
+
get_filename_component(INSTA360_SDK_DIR "${INSTA360_SDK_DIR}" ABSOLUTE)
|
|
22
|
+
set(INSTA360_SDK_LIB "${INSTA360_SDK_DIR}/lib/libCameraSDK.so")
|
|
23
|
+
if(NOT EXISTS "${INSTA360_SDK_DIR}/include/camera/camera.h" OR NOT EXISTS "${INSTA360_SDK_LIB}")
|
|
24
|
+
message(FATAL_ERROR
|
|
25
|
+
"INSTA360_SDK_DIR='${INSTA360_SDK_DIR}' does not look like the Insta360 CameraSDK: "
|
|
26
|
+
"expected include/camera/camera.h and lib/libCameraSDK.so.")
|
|
27
|
+
endif()
|
|
28
|
+
|
|
29
|
+
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module)
|
|
30
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
31
|
+
|
|
32
|
+
pybind11_add_module(_sdk MODULE
|
|
33
|
+
src/module.cc
|
|
34
|
+
src/py_camera.cc
|
|
35
|
+
src/stream_queues.cc)
|
|
36
|
+
target_include_directories(_sdk PRIVATE src "${INSTA360_SDK_DIR}/include")
|
|
37
|
+
target_compile_options(_sdk PRIVATE -Wall -Wextra)
|
|
38
|
+
target_link_libraries(_sdk PRIVATE "${INSTA360_SDK_LIB}")
|
|
39
|
+
|
|
40
|
+
# DT_RUNPATH (not DT_RPATH) so LD_LIBRARY_PATH can override the SDK location;
|
|
41
|
+
# the tests rely on that to substitute the fake SDK.
|
|
42
|
+
set_target_properties(_sdk PROPERTIES
|
|
43
|
+
BUILD_WITH_INSTALL_RPATH ON
|
|
44
|
+
INSTALL_RPATH "${INSTA360_SDK_DIR}/lib"
|
|
45
|
+
INSTALL_RPATH_USE_LINK_PATH OFF)
|
|
46
|
+
target_link_options(_sdk PRIVATE LINKER:--enable-new-dtags)
|
|
47
|
+
|
|
48
|
+
install(TARGETS _sdk LIBRARY DESTINATION pyinsta360)
|
pyinsta360-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chuoru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyinsta360
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for the Insta360 CameraSDK (X3 tested; X4/X5 nominal)
|
|
5
|
+
Keywords: insta360,camera,360,imu,pybind11,robotics
|
|
6
|
+
Author-Email: chuoru <tranvietthanh0101@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: C++
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Project-URL: Homepage, https://github.com/chuoru/pyinsta360
|
|
19
|
+
Project-URL: Issues, https://github.com/chuoru/pyinsta360/issues
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: numpy>=1.21
|
|
22
|
+
Provides-Extra: decode
|
|
23
|
+
Requires-Dist: av>=12; extra == "decode"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
26
|
+
Requires-Dist: av>=12; extra == "test"
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pyinsta360
|
|
30
|
+
|
|
31
|
+
Python bindings for the Insta360 CameraSDK: discover a camera, control it, and read its live
|
|
32
|
+
video, IMU and exposure streams from Python. Tested with an X3; X4 and X5 are nominal.
|
|
33
|
+
|
|
34
|
+
The SDK is proprietary and is **not** part of this repository. You supply your own copy.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
pyinsta360 ships as source only. It compiles on your machine against your own copy of the SDK,
|
|
39
|
+
which you get by applying at <https://www.insta360.com/sdk/apply>.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install pyinsta360 # from PyPI
|
|
43
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install 'pyinsta360[decode]' # plus PyAV for decoding
|
|
44
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install -e . # from a checkout
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`INSTA360_SDK_DIR` is the directory that contains `include/` and `lib/libCameraSDK.so`. The build
|
|
48
|
+
fails with a clear message if it is missing. The extension records `${INSTA360_SDK_DIR}/lib` as its
|
|
49
|
+
`DT_RUNPATH`, so the library is found without setting `LD_LIBRARY_PATH`, and `LD_LIBRARY_PATH` still
|
|
50
|
+
takes precedence if you set it. Needs a C++17 compiler, CMake 3.18+ and Python 3.10+. The SDK itself
|
|
51
|
+
needs glibc 2.17 / libstdc++ GLIBCXX_3.4.30, which means Ubuntu 22.04 or newer.
|
|
52
|
+
|
|
53
|
+
> A wheel built here links against, and embeds the path of, the proprietary SDK. Do not publish
|
|
54
|
+
> wheels built against it.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import pyinsta360 as pi
|
|
60
|
+
|
|
61
|
+
print(pi.sdk_version(), pi.discover()) # [] when no camera is attached
|
|
62
|
+
|
|
63
|
+
with pi.Camera() as cam: # or Camera("<serial>") to pick one
|
|
64
|
+
cam.start_live_streaming() # LiveStreamParam() defaults to 1920x960P30
|
|
65
|
+
for _ in range(100):
|
|
66
|
+
packet = cam.read_video(timeout=1.0) # VideoPacket (H.264/H.265 bytes) or None
|
|
67
|
+
imu = cam.read_imu() # structured array, every pending sample
|
|
68
|
+
...
|
|
69
|
+
print(cam.stream_stats()) # {'video_dropped': 0, 'imu_dropped': 0}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Decoding the stream to RGB frames (needs PyAV):
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from pyinsta360.decode import VideoDecoder
|
|
76
|
+
|
|
77
|
+
decoder = VideoDecoder(cam.video_encode_type())
|
|
78
|
+
for frame in decoder.decode(packet): # list of HxWx3 uint8 RGB arrays
|
|
79
|
+
...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`examples/stream_smoke.py` runs the whole path against a real camera and reports video fps, IMU rate
|
|
83
|
+
and drops: `python examples/stream_smoke.py --seconds 10 --save-frame first.png`.
|
|
84
|
+
|
|
85
|
+
Besides streaming, `Camera` offers battery and storage status, file list/download/delete, photo and
|
|
86
|
+
video capture, exposure and capture parameters, and clock sync. Enums such as `VideoResolution`,
|
|
87
|
+
`CameraFunctionMode` and `ExposureMode` are generated from the SDK headers. Type stubs ship in the
|
|
88
|
+
package.
|
|
89
|
+
|
|
90
|
+
## Units and clocks
|
|
91
|
+
|
|
92
|
+
Stream data is passed through in raw SDK units.
|
|
93
|
+
|
|
94
|
+
- IMU acceleration (`ax`, `ay`, `az`) is in **g**, not m/s^2. Multiply by 9.80665 for m/s^2. This
|
|
95
|
+
is the finding of the reference bridge, cross-checked against another driver that uses the same
|
|
96
|
+
SDK struct. Gyro (`gx`, `gy`, `gz`) is treated as rad/s there, which has not been verified
|
|
97
|
+
independently.
|
|
98
|
+
- Video and IMU `timestamp` values are in unspecified units and come from independent clocks: on
|
|
99
|
+
real hardware they sat tens of seconds apart at the start of one session. To align the two
|
|
100
|
+
streams, stamp each item with your own receipt time.
|
|
101
|
+
- The SDK delivers gyro data in bursts (about 50 samples every ~100 ms on an X3), so
|
|
102
|
+
`read_imu()` returns whole bursts, not a smooth trickle.
|
|
103
|
+
- Findings from the reference bridge worth knowing: `LiveStreamParam.using_lrv=True` delivered no
|
|
104
|
+
video in dual-lens (360) mode, and the camera may ignore the requested preview resolution.
|
|
105
|
+
|
|
106
|
+
## Known issues
|
|
107
|
+
|
|
108
|
+
- **SDK crash during discovery.** Reported upstream as [Insta360Develop/Desktop-CameraSDK-Cpp#95](https://github.com/Insta360Develop/Desktop-CameraSDK-Cpp/issues/95). Report and reproducer: `docs/sdk-bugs/discovery-crash/`. CameraSDK 2.1.8 sometimes aborts the process with `std::bad_function_call` during `discover()` or `Camera.open()`. The exception is thrown on the SDK's own session thread, so pyinsta360 cannot catch it. A plain C++ program reproduces it (4 of 10 runs under CPU load). In Python on an idle machine it hit between 0 of 10 and 6 of 10 runs. Retrying the process works, and `run_supervised` does that for you:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import pyinsta360
|
|
112
|
+
|
|
113
|
+
def main(): # module-level, so it can be pickled
|
|
114
|
+
with pyinsta360.Camera() as cam: ...
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
pyinsta360.run_supervised(main)
|
|
118
|
+
```
|
|
119
|
+
- **Wrong clip path from `stop_recording()`.** Reported upstream as [Insta360Develop/Desktop-CameraSDK-Cpp#96](https://github.com/Insta360Develop/Desktop-CameraSDK-Cpp/issues/96). Report and reproducer: `docs/sdk-bugs/stop-recording-url/`. With SDK 2.1.8 on an X3, the returned origin URL ended in `.lrv` while the file on the card ends in `.insv`. Downloading the returned path fails. pyinsta360 passes the SDK's URLs through unchanged. To find a new recording, diff `files_list()` from before and after.
|
|
120
|
+
- **Multiple video streams.** Some modes deliver each lens as its own stream. The X3 in `RES_2880_2880P30` mode sends `stream_index` 0 and 1 with independent timestamps. Group packets by `stream_index` and use one `VideoDecoder` per stream.
|
|
121
|
+
|
|
122
|
+
## Supervised runs
|
|
123
|
+
|
|
124
|
+
`run_supervised(target, *args, retries=3, **kwargs)` runs `target` in a spawned child process and
|
|
125
|
+
returns its result. If the child is killed by a signal while inside `discover()` or `Camera.open()`,
|
|
126
|
+
the target is rerun from the start (up to `retries` extra attempts, each logged as a warning on the
|
|
127
|
+
`pyinsta360` logger). A crash anywhere else, or a nonzero exit with no result, raises
|
|
128
|
+
`SupervisedProcessError` at once. Its `exitcode`, `attempts` and `during_discovery` say what
|
|
129
|
+
happened. Exceptions raised by `target` are re-raised in the parent.
|
|
130
|
+
|
|
131
|
+
- The target restarts from the beginning, so anything it did before `open()` repeats.
|
|
132
|
+
- `target`, its arguments and its result must be picklable. Use a module-level function.
|
|
133
|
+
- Notebooks that call `discover()` directly are not protected. Wrap the work in a function and
|
|
134
|
+
pass it to `run_supervised`.
|
|
135
|
+
|
|
136
|
+
## Threading model
|
|
137
|
+
|
|
138
|
+
- The SDK calls back on one of its own threads, and a video callback delays the gyro callback queued
|
|
139
|
+
behind it. The callbacks therefore only copy into bounded buffers and notify. They never touch
|
|
140
|
+
Python or the GIL.
|
|
141
|
+
- Buffers drop their **oldest** entries when full (`video_queue_size` 64 packets, `imu_queue_size`
|
|
142
|
+
20000 samples by default, exposure 256). Drops are counted in `stream_stats()`.
|
|
143
|
+
- Every call that can block releases the GIL, including `read_video`, which also wakes every
|
|
144
|
+
100 ms so Ctrl-C works.
|
|
145
|
+
- `close()` and `stop_live_streaming()` may be called from any thread. A thread blocked in
|
|
146
|
+
`read_video()` is woken and gets `None`. Reads on a camera that is not streaming return `None` or
|
|
147
|
+
an empty array, and a stop discards whatever was still buffered.
|
|
148
|
+
- Other camera calls are serialized by one lock, so `close()` waits for an SDK call that is already
|
|
149
|
+
running (for example a download) instead of racing it.
|
|
150
|
+
- The `progress(downloaded, total)` callback of `download_file` runs on an SDK thread with the GIL
|
|
151
|
+
held. An exception it raises is re-raised after the download returns, and the download is not
|
|
152
|
+
cancelled.
|
|
153
|
+
- Lifecycle is an explicit state, `Camera.state`: `CLOSED`, `OPEN`, `STREAMING`. A call that the
|
|
154
|
+
current state does not allow raises `pyinsta360.Insta360Error` (a `RuntimeError`), as does any
|
|
155
|
+
SDK action that returns failure, naming the method.
|
|
156
|
+
|
|
157
|
+
## Testing without hardware
|
|
158
|
+
|
|
159
|
+
`tests/fake_sdk/fake_camera_sdk.cc` is a stand-in `libCameraSDK.so` built against the real SDK
|
|
160
|
+
headers. It reports one X3 (serial `FAKE0001`), streams deterministic video packets at 30 Hz and
|
|
161
|
+
gyro batches of 17 samples, and has environment knobs for failure paths (see its header comment).
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
export INSTA360_SDK_DIR=/path/to/insta360_sdk
|
|
165
|
+
pip install -e '.[test]'
|
|
166
|
+
tools/test.sh # builds the fake, puts it first on LD_LIBRARY_PATH, runs pytest
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`LD_LIBRARY_PATH` has to be set before Python starts, which is why this is a wrapper script. Running
|
|
170
|
+
plain `pytest` stops with a message if it finds the real library. The suite also checks that every
|
|
171
|
+
`ins_camera` symbol the extension imports exists in both the fake and the real SDK, and that the
|
|
172
|
+
generated enums and type stubs are current. Hardware behavior is not covered; use the smoke example.
|
|
173
|
+
|
|
174
|
+
After changing the SDK headers, regenerate the enum bindings:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python tools/gen_enums.py --sdk-dir "$INSTA360_SDK_DIR" # add --check to only verify
|
|
178
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# pyinsta360
|
|
2
|
+
|
|
3
|
+
Python bindings for the Insta360 CameraSDK: discover a camera, control it, and read its live
|
|
4
|
+
video, IMU and exposure streams from Python. Tested with an X3; X4 and X5 are nominal.
|
|
5
|
+
|
|
6
|
+
The SDK is proprietary and is **not** part of this repository. You supply your own copy.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
pyinsta360 ships as source only. It compiles on your machine against your own copy of the SDK,
|
|
11
|
+
which you get by applying at <https://www.insta360.com/sdk/apply>.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install pyinsta360 # from PyPI
|
|
15
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install 'pyinsta360[decode]' # plus PyAV for decoding
|
|
16
|
+
INSTA360_SDK_DIR=/path/to/insta360_sdk pip install -e . # from a checkout
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`INSTA360_SDK_DIR` is the directory that contains `include/` and `lib/libCameraSDK.so`. The build
|
|
20
|
+
fails with a clear message if it is missing. The extension records `${INSTA360_SDK_DIR}/lib` as its
|
|
21
|
+
`DT_RUNPATH`, so the library is found without setting `LD_LIBRARY_PATH`, and `LD_LIBRARY_PATH` still
|
|
22
|
+
takes precedence if you set it. Needs a C++17 compiler, CMake 3.18+ and Python 3.10+. The SDK itself
|
|
23
|
+
needs glibc 2.17 / libstdc++ GLIBCXX_3.4.30, which means Ubuntu 22.04 or newer.
|
|
24
|
+
|
|
25
|
+
> A wheel built here links against, and embeds the path of, the proprietary SDK. Do not publish
|
|
26
|
+
> wheels built against it.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import pyinsta360 as pi
|
|
32
|
+
|
|
33
|
+
print(pi.sdk_version(), pi.discover()) # [] when no camera is attached
|
|
34
|
+
|
|
35
|
+
with pi.Camera() as cam: # or Camera("<serial>") to pick one
|
|
36
|
+
cam.start_live_streaming() # LiveStreamParam() defaults to 1920x960P30
|
|
37
|
+
for _ in range(100):
|
|
38
|
+
packet = cam.read_video(timeout=1.0) # VideoPacket (H.264/H.265 bytes) or None
|
|
39
|
+
imu = cam.read_imu() # structured array, every pending sample
|
|
40
|
+
...
|
|
41
|
+
print(cam.stream_stats()) # {'video_dropped': 0, 'imu_dropped': 0}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Decoding the stream to RGB frames (needs PyAV):
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pyinsta360.decode import VideoDecoder
|
|
48
|
+
|
|
49
|
+
decoder = VideoDecoder(cam.video_encode_type())
|
|
50
|
+
for frame in decoder.decode(packet): # list of HxWx3 uint8 RGB arrays
|
|
51
|
+
...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`examples/stream_smoke.py` runs the whole path against a real camera and reports video fps, IMU rate
|
|
55
|
+
and drops: `python examples/stream_smoke.py --seconds 10 --save-frame first.png`.
|
|
56
|
+
|
|
57
|
+
Besides streaming, `Camera` offers battery and storage status, file list/download/delete, photo and
|
|
58
|
+
video capture, exposure and capture parameters, and clock sync. Enums such as `VideoResolution`,
|
|
59
|
+
`CameraFunctionMode` and `ExposureMode` are generated from the SDK headers. Type stubs ship in the
|
|
60
|
+
package.
|
|
61
|
+
|
|
62
|
+
## Units and clocks
|
|
63
|
+
|
|
64
|
+
Stream data is passed through in raw SDK units.
|
|
65
|
+
|
|
66
|
+
- IMU acceleration (`ax`, `ay`, `az`) is in **g**, not m/s^2. Multiply by 9.80665 for m/s^2. This
|
|
67
|
+
is the finding of the reference bridge, cross-checked against another driver that uses the same
|
|
68
|
+
SDK struct. Gyro (`gx`, `gy`, `gz`) is treated as rad/s there, which has not been verified
|
|
69
|
+
independently.
|
|
70
|
+
- Video and IMU `timestamp` values are in unspecified units and come from independent clocks: on
|
|
71
|
+
real hardware they sat tens of seconds apart at the start of one session. To align the two
|
|
72
|
+
streams, stamp each item with your own receipt time.
|
|
73
|
+
- The SDK delivers gyro data in bursts (about 50 samples every ~100 ms on an X3), so
|
|
74
|
+
`read_imu()` returns whole bursts, not a smooth trickle.
|
|
75
|
+
- Findings from the reference bridge worth knowing: `LiveStreamParam.using_lrv=True` delivered no
|
|
76
|
+
video in dual-lens (360) mode, and the camera may ignore the requested preview resolution.
|
|
77
|
+
|
|
78
|
+
## Known issues
|
|
79
|
+
|
|
80
|
+
- **SDK crash during discovery.** Reported upstream as [Insta360Develop/Desktop-CameraSDK-Cpp#95](https://github.com/Insta360Develop/Desktop-CameraSDK-Cpp/issues/95). Report and reproducer: `docs/sdk-bugs/discovery-crash/`. CameraSDK 2.1.8 sometimes aborts the process with `std::bad_function_call` during `discover()` or `Camera.open()`. The exception is thrown on the SDK's own session thread, so pyinsta360 cannot catch it. A plain C++ program reproduces it (4 of 10 runs under CPU load). In Python on an idle machine it hit between 0 of 10 and 6 of 10 runs. Retrying the process works, and `run_supervised` does that for you:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import pyinsta360
|
|
84
|
+
|
|
85
|
+
def main(): # module-level, so it can be pickled
|
|
86
|
+
with pyinsta360.Camera() as cam: ...
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
pyinsta360.run_supervised(main)
|
|
90
|
+
```
|
|
91
|
+
- **Wrong clip path from `stop_recording()`.** Reported upstream as [Insta360Develop/Desktop-CameraSDK-Cpp#96](https://github.com/Insta360Develop/Desktop-CameraSDK-Cpp/issues/96). Report and reproducer: `docs/sdk-bugs/stop-recording-url/`. With SDK 2.1.8 on an X3, the returned origin URL ended in `.lrv` while the file on the card ends in `.insv`. Downloading the returned path fails. pyinsta360 passes the SDK's URLs through unchanged. To find a new recording, diff `files_list()` from before and after.
|
|
92
|
+
- **Multiple video streams.** Some modes deliver each lens as its own stream. The X3 in `RES_2880_2880P30` mode sends `stream_index` 0 and 1 with independent timestamps. Group packets by `stream_index` and use one `VideoDecoder` per stream.
|
|
93
|
+
|
|
94
|
+
## Supervised runs
|
|
95
|
+
|
|
96
|
+
`run_supervised(target, *args, retries=3, **kwargs)` runs `target` in a spawned child process and
|
|
97
|
+
returns its result. If the child is killed by a signal while inside `discover()` or `Camera.open()`,
|
|
98
|
+
the target is rerun from the start (up to `retries` extra attempts, each logged as a warning on the
|
|
99
|
+
`pyinsta360` logger). A crash anywhere else, or a nonzero exit with no result, raises
|
|
100
|
+
`SupervisedProcessError` at once. Its `exitcode`, `attempts` and `during_discovery` say what
|
|
101
|
+
happened. Exceptions raised by `target` are re-raised in the parent.
|
|
102
|
+
|
|
103
|
+
- The target restarts from the beginning, so anything it did before `open()` repeats.
|
|
104
|
+
- `target`, its arguments and its result must be picklable. Use a module-level function.
|
|
105
|
+
- Notebooks that call `discover()` directly are not protected. Wrap the work in a function and
|
|
106
|
+
pass it to `run_supervised`.
|
|
107
|
+
|
|
108
|
+
## Threading model
|
|
109
|
+
|
|
110
|
+
- The SDK calls back on one of its own threads, and a video callback delays the gyro callback queued
|
|
111
|
+
behind it. The callbacks therefore only copy into bounded buffers and notify. They never touch
|
|
112
|
+
Python or the GIL.
|
|
113
|
+
- Buffers drop their **oldest** entries when full (`video_queue_size` 64 packets, `imu_queue_size`
|
|
114
|
+
20000 samples by default, exposure 256). Drops are counted in `stream_stats()`.
|
|
115
|
+
- Every call that can block releases the GIL, including `read_video`, which also wakes every
|
|
116
|
+
100 ms so Ctrl-C works.
|
|
117
|
+
- `close()` and `stop_live_streaming()` may be called from any thread. A thread blocked in
|
|
118
|
+
`read_video()` is woken and gets `None`. Reads on a camera that is not streaming return `None` or
|
|
119
|
+
an empty array, and a stop discards whatever was still buffered.
|
|
120
|
+
- Other camera calls are serialized by one lock, so `close()` waits for an SDK call that is already
|
|
121
|
+
running (for example a download) instead of racing it.
|
|
122
|
+
- The `progress(downloaded, total)` callback of `download_file` runs on an SDK thread with the GIL
|
|
123
|
+
held. An exception it raises is re-raised after the download returns, and the download is not
|
|
124
|
+
cancelled.
|
|
125
|
+
- Lifecycle is an explicit state, `Camera.state`: `CLOSED`, `OPEN`, `STREAMING`. A call that the
|
|
126
|
+
current state does not allow raises `pyinsta360.Insta360Error` (a `RuntimeError`), as does any
|
|
127
|
+
SDK action that returns failure, naming the method.
|
|
128
|
+
|
|
129
|
+
## Testing without hardware
|
|
130
|
+
|
|
131
|
+
`tests/fake_sdk/fake_camera_sdk.cc` is a stand-in `libCameraSDK.so` built against the real SDK
|
|
132
|
+
headers. It reports one X3 (serial `FAKE0001`), streams deterministic video packets at 30 Hz and
|
|
133
|
+
gyro batches of 17 samples, and has environment knobs for failure paths (see its header comment).
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export INSTA360_SDK_DIR=/path/to/insta360_sdk
|
|
137
|
+
pip install -e '.[test]'
|
|
138
|
+
tools/test.sh # builds the fake, puts it first on LD_LIBRARY_PATH, runs pytest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`LD_LIBRARY_PATH` has to be set before Python starts, which is why this is a wrapper script. Running
|
|
142
|
+
plain `pytest` stops with a message if it finds the real library. The suite also checks that every
|
|
143
|
+
`ins_camera` symbol the extension imports exists in both the fake and the real SDK, and that the
|
|
144
|
+
generated enums and type stubs are current. Hardware behavior is not covered; use the smoke example.
|
|
145
|
+
|
|
146
|
+
After changing the SDK headers, regenerate the enum bindings:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python tools/gen_enums.py --sdk-dir "$INSTA360_SDK_DIR" # add --check to only verify
|
|
150
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# CameraSDK 2.1.8: `DeviceDiscovery::GetAvailableDevices` intermittently aborts the process with `std::bad_function_call`
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`ins_camera::DeviceDiscovery::GetAvailableDevices()` sometimes terminates the whole process. An internal SDK thread, `ins_camera::connection::CameraSession::ReadLoop()`, invokes an empty `std::function`. The resulting `std::bad_function_call` is not caught on that thread, so `std::terminate` aborts the process (SIGABRT, exit code 134). The calling application cannot catch or recover from it.
|
|
6
|
+
|
|
7
|
+
It reproduces with a minimal C++ program that only discovers and opens a camera. It happens more often when the CPU is busy, which points to a race inside the SDK.
|
|
8
|
+
|
|
9
|
+
## Environment
|
|
10
|
+
|
|
11
|
+
- CameraSDK 2.1.8 (Linux x86_64, `lib/libCameraSDK.so`)
|
|
12
|
+
- Camera: Insta360 X3, firmware v1.1.6, connected over USB (vid:pid `2e1a:0002`)
|
|
13
|
+
- OS: Ubuntu 24.04.5 LTS, kernel 7.0.0, glibc 2.39
|
|
14
|
+
- Compiler: GCC 13.3.0, `-std=c++17`
|
|
15
|
+
- CPU: Intel Core i9-13900HX (32 threads)
|
|
16
|
+
|
|
17
|
+
## Steps to reproduce
|
|
18
|
+
|
|
19
|
+
1. Connect an X3 over USB and make sure the user can open the USB device.
|
|
20
|
+
2. Build and run the attached `repro.cc`, which only calls `GetAvailableDevices`, `Camera::Open`, `FreeDeviceDescriptors` and `Close`. The attached `run.sh` builds it and runs it repeatedly with every CPU core busy:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
INSTA360_SDK_DIR=/path/to/CameraSDK ./run.sh 30
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Some runs exit with code 134 and print:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
terminate called after throwing an instance of 'std::bad_function_call'
|
|
30
|
+
what(): bad_function_call
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Expected
|
|
34
|
+
|
|
35
|
+
`GetAvailableDevices()` returns the device list, or an empty list on failure. It never terminates the process.
|
|
36
|
+
|
|
37
|
+
## Actual
|
|
38
|
+
|
|
39
|
+
The process aborts during `GetAvailableDevices()`. `repro.cc` prints a `STEP` line before each SDK call. In every crash the last line printed was `STEP discover`, so the abort happens inside `GetAvailableDevices()` and never in `Open()`, `FreeDeviceDescriptors()` or `Close()`.
|
|
40
|
+
|
|
41
|
+
Measured results:
|
|
42
|
+
|
|
43
|
+
| Program | Condition | Crashed |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| discover + open (`repro.cc`) | all cores busy | 7 of 72 runs, all in `GetAvailableDevices` |
|
|
46
|
+
| discover only | all cores busy | 4 of 10 runs |
|
|
47
|
+
| discover only | idle | 0 of 12 runs |
|
|
48
|
+
| The same calls through a Python binding | idle | between 0 of 10 and 6 of 10 runs per batch |
|
|
49
|
+
|
|
50
|
+
## Stack trace
|
|
51
|
+
|
|
52
|
+
Captured with gdb on the crashing thread (frames below `ReadLoop` are libstdc++'s thread start):
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
#7 std::terminate() libstdc++.so.6
|
|
56
|
+
#8 __cxa_throw () libstdc++.so.6
|
|
57
|
+
#9 std::__throw_bad_function_call() libstdc++.so.6
|
|
58
|
+
#10 ins_camera::connection::CameraSession::ReadLoop() libCameraSDK.so
|
|
59
|
+
#11 ?? (std::thread start routine) libstdc++.so.6
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Just before the abort, the SDK log (level VERBOSE) shows the session's first heartbeat:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
I camera_session.cpp:0275: heartbeat is coming
|
|
66
|
+
I camera_session.cpp:0277: package version: 0
|
|
67
|
+
I camera_session.cpp:0250: first heart beat package is coming
|
|
68
|
+
I camera_session.cpp:0063: heartbeat package is coming
|
|
69
|
+
terminate called after throwing an instance of 'std::bad_function_call'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Analysis
|
|
73
|
+
|
|
74
|
+
These points are inferences, not confirmed against SDK source:
|
|
75
|
+
|
|
76
|
+
- `ReadLoop` calls a `std::function` member (a packet or heartbeat handler) that is empty at that moment. It seems to be read without synchronization while it is being set or cleared, for example while the temporary session that `GetAvailableDevices` opens to read the serial number is set up or torn down.
|
|
77
|
+
- The application cannot work around it inside the process. We tried a `std::set_terminate` handler that recognizes this exception on the SDK thread:
|
|
78
|
+
- calling `pthread_exit` from the handler re-enters `std::terminate` (the thread's start routine is `noexcept`) until the process segfaults;
|
|
79
|
+
- parking the thread in the handler makes `GetAvailableDevices()` hang, because the SDK waits for that thread to finish.
|
|
80
|
+
|
|
81
|
+
## Suggested fix
|
|
82
|
+
|
|
83
|
+
Ensure `ReadLoop` never invokes an empty handler. Either check it before calling (`if (handler) handler(...)`) under the same lock that guards assignment, or set the handler before the read thread starts and clear it only after the thread has been joined. Catching exceptions at the top of `ReadLoop` would also stop a single bad packet from killing the host process.
|
|
84
|
+
|
|
85
|
+
## Impact
|
|
86
|
+
|
|
87
|
+
Any application that discovers or opens a camera can be killed at startup, at random. We currently run discovery in a supervised child process and restart it when it aborts.
|
|
88
|
+
|
|
89
|
+
## Attachments
|
|
90
|
+
|
|
91
|
+
- `repro.cc` is the minimal reproducer.
|
|
92
|
+
- `run.sh` builds it and runs it under CPU load.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#include <camera/camera.h>
|
|
2
|
+
#include <camera/device_discovery.h>
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
int main() {
|
|
5
|
+
ins_camera::SetLogLevel(ins_camera::LogLevel::ERR);
|
|
6
|
+
ins_camera::DeviceDiscovery discovery;
|
|
7
|
+
fprintf(stderr, "STEP discover\n");
|
|
8
|
+
auto list = discovery.GetAvailableDevices();
|
|
9
|
+
if (list.empty()) { fprintf(stderr, "STEP none\n"); return 3; }
|
|
10
|
+
fprintf(stderr, "STEP construct\n");
|
|
11
|
+
auto cam = std::make_shared<ins_camera::Camera>(list[0].info);
|
|
12
|
+
fprintf(stderr, "STEP open\n");
|
|
13
|
+
bool ok = cam->Open();
|
|
14
|
+
fprintf(stderr, "STEP free (open=%d)\n", ok);
|
|
15
|
+
discovery.FreeDeviceDescriptors(list);
|
|
16
|
+
fprintf(stderr, "STEP close\n");
|
|
17
|
+
cam->Close();
|
|
18
|
+
fprintf(stderr, "STEP done\n");
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds repro.cc against the CameraSDK and runs it N times with every core busy.
|
|
3
|
+
# Usage: INSTA360_SDK_DIR=/path/to/sdk ./run.sh [runs]
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
: "${INSTA360_SDK_DIR:?set INSTA360_SDK_DIR to the CameraSDK directory}"
|
|
6
|
+
runs="${1:-30}"
|
|
7
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
out="$(mktemp -d)"
|
|
9
|
+
g++ -std=c++17 -g -I"$INSTA360_SDK_DIR/include" "$here/repro.cc" \
|
|
10
|
+
-L"$INSTA360_SDK_DIR/lib" -lCameraSDK -Wl,-rpath,"$INSTA360_SDK_DIR/lib" -o "$out/repro" || exit 1
|
|
11
|
+
crashes=0
|
|
12
|
+
for i in $(seq 1 "$runs"); do
|
|
13
|
+
for _ in $(seq 1 "$(nproc)"); do (timeout 5 sh -c 'while :; do :; done' &); done
|
|
14
|
+
timeout 40 "$out/repro" > "$out/run_$i.txt" 2>&1
|
|
15
|
+
code=$?
|
|
16
|
+
if [ "$code" -ne 0 ]; then
|
|
17
|
+
crashes=$((crashes + 1))
|
|
18
|
+
echo "run $i: exit $code, last step: $(grep STEP "$out/run_$i.txt" | tail -1)"
|
|
19
|
+
fi
|
|
20
|
+
sleep 5
|
|
21
|
+
done
|
|
22
|
+
echo "crashed $crashes of $runs runs (logs in $out)"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# CameraSDK 2.1.8: `Camera::StopRecording()` returns an origin URL with the wrong extension (`.lrv` instead of `.insv`)
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
On an Insta360 X3, the `MediaUrl` returned by `Camera::StopRecording()` names an origin file that does not exist. The returned origin path ends in `.lrv`, while the recorded file on the camera ends in `.insv`. Downloading the returned path with `DownloadCameraFile` fails, while the `.insv` path downloads correctly. Applications that use the returned URL to fetch a recording cannot get the file.
|
|
6
|
+
|
|
7
|
+
## Environment
|
|
8
|
+
|
|
9
|
+
- CameraSDK 2.1.8 (Linux x86_64, `lib/libCameraSDK.so`)
|
|
10
|
+
- Camera: Insta360 X3, firmware v1.1.6, connected over USB (vid:pid `2e1a:0002`)
|
|
11
|
+
- OS: Ubuntu 24.04.5 LTS, kernel 7.0.0, glibc 2.39
|
|
12
|
+
- Camera state before recording: `GetCurrentFunctionMode()` returned `FUNCTION_MODE_HDR_IMAGE`. No `SetVideoCaptureParams` call was made before `StartRecording()`.
|
|
13
|
+
|
|
14
|
+
## Steps to reproduce
|
|
15
|
+
|
|
16
|
+
1. Connect an X3 over USB.
|
|
17
|
+
2. Build and run the attached `repro.cc`. It records for 3 seconds, prints the returned `MediaUrl`, compares it with the camera's file list, and tries to download each returned path:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
g++ -std=c++17 -I$SDK/include repro.cc -L$SDK/lib -lCameraSDK -Wl,-rpath,$SDK/lib -o repro
|
|
21
|
+
./repro 3 --delete # --delete removes the clip afterwards
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Expected
|
|
25
|
+
|
|
26
|
+
`MediaUrl::OriginUrls()` lists the recorded `.insv` file, and `LRVUrls()` lists its `.lrv` proxy. Every returned path exists in `GetCameraFilesList()` and can be downloaded.
|
|
27
|
+
|
|
28
|
+
## Actual
|
|
29
|
+
|
|
30
|
+
Observed on 2026-11-16 (3-second recording):
|
|
31
|
+
|
|
32
|
+
| Source | Path |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `StopRecording().OriginUrls()` | `/DCIM/Camera01/VID_20261116_051448_00_007.lrv` |
|
|
35
|
+
| `StopRecording().LRVUrls()` | `/DCIM/Camera01/LRV_20261116_051448_01_007.lrv` |
|
|
36
|
+
| New in `GetCameraFilesList()` | `/DCIM/Camera01/VID_20261116_051448_00_007.insv` |
|
|
37
|
+
| New in `GetCameraFilesList()` | `/DCIM/Camera01/LRV_20261116_051448_01_007.lrv` |
|
|
38
|
+
|
|
39
|
+
- `DownloadCameraFile("/DCIM/Camera01/VID_20261116_051448_00_007.lrv", ...)` returned `false`.
|
|
40
|
+
- `DownloadCameraFile("/DCIM/Camera01/VID_20261116_051448_00_007.insv", ...)` returned `true` and produced a valid 22,028,003-byte file (3840x1920 H.264 video with AAC audio, 3.27 s, 98 frames).
|
|
41
|
+
- The `LRVUrls()` entry was correct and present on the camera.
|
|
42
|
+
- `TakePhoto()` in the same session returned a correct path (`IMG_20261116_051438_00_006.insp`), so the problem appears specific to `StopRecording()`.
|
|
43
|
+
|
|
44
|
+
The origin URL has the right base name (`VID_…_00_007`) and the wrong extension. It looks like the extension is taken from the LRV entry when the origin URL is built. That is an inference from the output, not confirmed.
|
|
45
|
+
|
|
46
|
+
## Reproduced with the plain C++ API
|
|
47
|
+
|
|
48
|
+
The first observation was made through a Python binding that copies `MediaUrl::OriginUrls()` and `LRVUrls()` without modification. The attached `repro.cc` makes the same calls directly against the C++ API, with no binding involved. Run on the same camera on 2026-11-16 (`./repro 3 --delete`, camera in function mode 8, `FUNCTION_MODE_HDR_IMAGE`), it reproduced the problem:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
returned origin: /DCIM/Camera01/VID_20261116_055409_00_006.lrv
|
|
52
|
+
returned lrv: /DCIM/Camera01/LRV_20261116_055409_01_006.lrv
|
|
53
|
+
new on camera: /DCIM/Camera01/LRV_20261116_055409_01_006.lrv
|
|
54
|
+
new on camera: /DCIM/Camera01/VID_20261116_055409_00_006.insv
|
|
55
|
+
BAD listed=0 downloaded=0 /DCIM/Camera01/VID_20261116_055409_00_006.lrv
|
|
56
|
+
OK listed=1 downloaded=1 /DCIM/Camera01/LRV_20261116_055409_01_006.lrv
|
|
57
|
+
RESULT: returned MediaUrl names a file that does not exist
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Two out of two recordings showed the same wrong extension.
|
|
61
|
+
|
|
62
|
+
## Suggested fix
|
|
63
|
+
|
|
64
|
+
Build each origin URL from the recorded file's own name, with its `.insv` extension, and check that every returned path exists in the camera's file list before returning it.
|
|
65
|
+
|
|
66
|
+
## Impact
|
|
67
|
+
|
|
68
|
+
Any application that downloads a recording right after `StopRecording()` fails to fetch it. The current workaround is to compare `GetCameraFilesList()` from before and after the recording.
|
|
69
|
+
|
|
70
|
+
## Attachments
|
|
71
|
+
|
|
72
|
+
- `repro.cc` is the reproducer. It exits 1 when a returned path is missing or cannot be downloaded.
|