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.
Files changed (34) hide show
  1. pyinsta360-0.1.0/CMakeLists.txt +48 -0
  2. pyinsta360-0.1.0/LICENSE +21 -0
  3. pyinsta360-0.1.0/PKG-INFO +178 -0
  4. pyinsta360-0.1.0/README.md +150 -0
  5. pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/REPORT.md +92 -0
  6. pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/repro.cc +20 -0
  7. pyinsta360-0.1.0/docs/sdk-bugs/discovery-crash/run.sh +22 -0
  8. pyinsta360-0.1.0/docs/sdk-bugs/stop-recording-url/REPORT.md +72 -0
  9. pyinsta360-0.1.0/docs/sdk-bugs/stop-recording-url/repro.cc +58 -0
  10. pyinsta360-0.1.0/examples/stream_smoke.py +77 -0
  11. pyinsta360-0.1.0/pyproject.toml +45 -0
  12. pyinsta360-0.1.0/python/pyinsta360/__init__.py +94 -0
  13. pyinsta360-0.1.0/python/pyinsta360/_guard.py +21 -0
  14. pyinsta360-0.1.0/python/pyinsta360/_sdk.pyi +197 -0
  15. pyinsta360-0.1.0/python/pyinsta360/_sdk_enums.pyi +462 -0
  16. pyinsta360-0.1.0/python/pyinsta360/decode.py +57 -0
  17. pyinsta360-0.1.0/python/pyinsta360/py.typed +0 -0
  18. pyinsta360-0.1.0/python/pyinsta360/supervise.py +112 -0
  19. pyinsta360-0.1.0/src/enums_gen.inc +452 -0
  20. pyinsta360-0.1.0/src/module.cc +432 -0
  21. pyinsta360-0.1.0/src/py_camera.cc +323 -0
  22. pyinsta360-0.1.0/src/py_camera.h +131 -0
  23. pyinsta360-0.1.0/src/stream_queues.cc +104 -0
  24. pyinsta360-0.1.0/src/stream_queues.h +68 -0
  25. pyinsta360-0.1.0/tests/conftest.py +70 -0
  26. pyinsta360-0.1.0/tests/fake_sdk/fake_camera_sdk.cc +439 -0
  27. pyinsta360-0.1.0/tests/supervise_targets.py +38 -0
  28. pyinsta360-0.1.0/tests/test_build.py +100 -0
  29. pyinsta360-0.1.0/tests/test_camera.py +572 -0
  30. pyinsta360-0.1.0/tests/test_decode.py +82 -0
  31. pyinsta360-0.1.0/tests/test_supervise.py +65 -0
  32. pyinsta360-0.1.0/tools/build_fake_sdk.sh +13 -0
  33. pyinsta360-0.1.0/tools/gen_enums.py +116 -0
  34. 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)
@@ -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.