├── .github └── FUNDING.yml ├── .gitignore ├── BUILDING.md ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── audio ├── CMakeLists.txt ├── audio.cpp └── audio.hpp ├── core ├── CMakeLists.txt ├── completed_request.hpp ├── frame_info.hpp ├── metadata.hpp └── stream_info.hpp ├── dispmanx ├── CMakeLists.txt ├── dispmanx.c └── dispmanx.h ├── etc ├── README.md ├── default │ └── picam └── init.d │ └── picam ├── httplivestreaming ├── CMakeLists.txt ├── httplivestreaming.c └── httplivestreaming.h ├── images ├── subtitle_example1.png ├── subtitle_example1_small.png ├── subtitle_example2.png ├── subtitle_example2_small.png ├── subtitle_intro.png ├── subtitle_intro_small.png └── youtube.png ├── libhook ├── CMakeLists.txt ├── hook.c └── hook.h ├── libpicam ├── CMakeLists.txt ├── main.cpp ├── picam.cpp └── picam.hpp ├── libstate ├── CMakeLists.txt ├── state.c └── state.h ├── log ├── CMakeLists.txt ├── log.c └── log.h ├── mpegts ├── CMakeLists.txt ├── mpegts.c └── mpegts.h ├── muxer ├── CMakeLists.txt ├── muxer.cpp └── muxer.hpp ├── picam_option ├── CMakeLists.txt ├── picam_option.cpp └── picam_option.hpp ├── preview ├── CMakeLists.txt ├── drm_preview.cpp ├── egl_preview.cpp ├── null_preview.cpp ├── preview.cpp └── preview.hpp ├── rtsp ├── CMakeLists.txt ├── rtsp.c └── rtsp.h ├── subtitle ├── CMakeLists.txt ├── subtitle.c └── subtitle.h ├── text ├── CMakeLists.txt ├── text.c └── text.h ├── timestamp ├── CMakeLists.txt ├── timestamp.c └── timestamp.h └── video_encoder ├── CMakeLists.txt ├── video_encoder.cpp └── video_encoder.hpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: iizukanao 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | 5 | # Libraries 6 | *.lib 7 | *.a 8 | 9 | # Shared objects (inc. Windows DLLs) 10 | *.dll 11 | *.so 12 | *.so.* 13 | *.dylib 14 | 15 | # Executables 16 | *.exe 17 | *.out 18 | *.app 19 | picam 20 | build/ 21 | 22 | *~ 23 | *.sw[po] 24 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # picam build instructions 2 | 3 | This file explains how to build picam yourself. The whole process takes under an hour on Raspberry Pi 2 or 3. 4 | 5 | 6 | # Steps 7 | 8 | ## Install required packages 9 | 10 | ```sh 11 | $ sudo apt-get install git cmake libcamera-dev libharfbuzz-dev libfontconfig-dev libasound-dev libdrm-dev libegl-dev libepoxy-dev libssl-dev liblzma-dev 12 | ``` 13 | 14 | (NOTE: `$` denotes command prompt. Do not enter `$` when entering commands.) 15 | 16 | 17 | ## Build and install fdk-aac 18 | 19 | Install [fdk-aac](https://sourceforge.net/projects/opencore-amr/files/fdk-aac/) with the following commands. 20 | 21 | ```sh 22 | $ wget https://downloads.sourceforge.net/project/opencore-amr/fdk-aac/fdk-aac-2.0.3.tar.gz 23 | $ tar zxvf fdk-aac-2.0.3.tar.gz 24 | $ cd fdk-aac-2.0.3 25 | $ ./configure 26 | $ make -j4 27 | (takes 3-4 minutes) 28 | $ sudo make install 29 | ``` 30 | 31 | 32 | ## Build and install ffmpeg 33 | 34 | NOTE: **Do not use `apt-get` for installing ffmpeg.** 35 | 36 | Download ffmpeg source and configure it: 37 | 38 | ```sh 39 | $ git clone https://git.ffmpeg.org/ffmpeg.git 40 | $ cd ffmpeg 41 | $ ./configure --enable-libfdk-aac 42 | (takes about one minute) 43 | ``` 44 | 45 | In the output of `configure`, make sure that there is `libfdk_aac` in `Enabled encoders`. 46 | 47 | Run the following commands to build and install ffmpeg. 48 | 49 | ```sh 50 | $ make -j4 51 | (takes 25-40 minutes) 52 | $ sudo make install 53 | ``` 54 | 55 | Run `ldconfig` in order to resolve dynamic linker problems. 56 | 57 | ```sh 58 | $ sudo ldconfig 59 | ``` 60 | 61 | 62 | ## Build picam 63 | 64 | ```sh 65 | $ git clone https://github.com/iizukanao/picam.git 66 | $ cd picam 67 | $ mkdir build 68 | $ cd build 69 | $ cmake .. 70 | $ make -j4 71 | ``` 72 | 73 | You can save some disk space by running `strip`. 74 | 75 | ```sh 76 | $ strip picam 77 | ``` 78 | 79 | Check if picam runs without errors. 80 | 81 | ``` 82 | $ ./picam --help 83 | ``` 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 2.0.12 *(2023-02-16)* 5 | ---------------------------- 6 | 7 | - Added Noise Gate feature (`--ngate` option). (by @francescovannini) 8 | 9 | Version 2.0.11 *(2023-01-31)* 10 | ---------------------------- 11 | 12 | - Enable HDR mode by default on Camera Module 3. 13 | - Added `--nohdr` option (only affects if Camera Module 3 is used). 14 | 15 | Version 2.0.10 *(2023-01-30)* 16 | ---------------------------- 17 | 18 | - Added `--autofocus-mode` and `--lens-position` options for Camera Module 3. 19 | 20 | Version 2.0.9 *(2022-12-07)* 21 | ---------------------------- 22 | 23 | - Built binary for the latest version of libcamera. (#193) 24 | 25 | Version 2.0.8 *(2022-10-22)* 26 | ---------------------------- 27 | 28 | - Updated binary for the new libcamera. (#187) 29 | 30 | Version 2.0.7 *(2022-09-26)* 31 | ---------------------------- 32 | 33 | - Added "brightness" hook. 34 | - Added "contrast" hook. 35 | - Added "saturation" hook. 36 | - Added "sharpness" hook. 37 | 38 | Version 2.0.6 *(2022-09-09)* 39 | ---------------------------- 40 | 41 | - Fixed "symbol lookup error". (#180) 42 | 43 | Version 2.0.5 *(2022-08-15)* 44 | ---------------------------- 45 | 46 | - Fixed a bug that caused segmentation fault when filename was specified in `hooks/start_record` file. (#177) 47 | 48 | Version 2.0.4 *(2022-08-10)* 49 | ---------------------------- 50 | 51 | - Changed the default values for the following options: 52 | + `--avcprofile` default is `high` (previously `baseline`) 53 | + `-v, --videobitrate` default is `3000000` (previously `4500000`) 54 | - Fixed memory leak. (#175) 55 | - Fixed bug where HLS was not working on 64-bit OS. (#172) 56 | 57 | Version 2.0.3 *(2022-07-25)* 58 | ---------------------------- 59 | 60 | - Fixed bug where sending SIGTERM does not stop picam. 61 | 62 | Version 2.0.2 *(2022-07-21)* 63 | ---------------------------- 64 | 65 | - Added the following options. (by @cstillwell89) 66 | + `--brightness` 67 | + `--contrast` 68 | + `--saturation` 69 | + `--sharpness` 70 | 71 | Version 2.0.1 *(2022-07-17)* 72 | ---------------------------- 73 | 74 | - Added `--camera` option. 75 | - Fixed `--version` option. 76 | 77 | Version 2.0.0 *(2022-07-07)* 78 | ---------------------------- 79 | 80 | - Built with libcamera. 81 | - Stopped using legacy camera libraries (OpenMAX IL and MMAL). 82 | - Added support for higher video resolutions up to 1920x1080 at 30 fps. 83 | - Changed the default values for the following options: 84 | + `-w, --width` default is `1920` (previously `1280`) 85 | + `-h, --height` default is `1080` (previously `720`) 86 | + `-v, --videobitrate` default is `4500000` (previously `2000000`) 87 | + `--avcprofile` default is `baseline` (previously `constrained_baseline`) 88 | + `--avclevel` default is `4.1` (previously `3.1`) 89 | - Added command line option `--hdmi` which selects HDMI port for video preview. Only works in console mode. 90 | - Removed the following options due to technical limitations: 91 | + `--rotation` (For 180 degree rotation, use `--hflip --vflip` instead) 92 | + `--qpmin` 93 | + `--qpmax` 94 | + `--qpinit` 95 | + `--dquant` 96 | + `--aperture` 97 | + `--iso` 98 | + `--opacity` 99 | + `--blank` 100 | + `--mode` 101 | - Changed the values for the following options. For the available values, please run `picam --help`. 102 | + `--ex` 103 | + `--wb` 104 | + `--metering` 105 | - For NoIR camera users, `--wb greyworld` option is no longer available. Instead, pass `LIBCAMERA_RPI_TUNING_FILE` environment variable to picam like this: `LIBCAMERA_RPI_TUNING_FILE=/usr/share/libcamera/ipa/raspberrypi/ov5647_noir.json ./picam` 106 | 107 | ### Known issues 108 | 109 | - There are some noise in audio preview (`--audiopreview` option) if video resolution is 1920x1080. 110 | - If X Window System (desktop environment) is running, video fps will drop due to system load. 111 | - EGL preview does not work on Raspberry Pi 3. 112 | - Some problems may occur in the EGL preview. 113 | 114 | Version 1.4.11 *(2021-04-29)* 115 | ----------------------------- 116 | 117 | - Fixed flicker between subtitle changes (#159) 118 | 119 | Version 1.4.10 *(2021-02-04)* 120 | ----------------------------- 121 | 122 | - Fixed HLS issue (#152) (by @marler8997) 123 | - Fixed build issues with the latest ffmpeg (by @marler8997) 124 | 125 | Version 1.4.9 *(2020-07-13)* 126 | ----------------------------- 127 | 128 | - Added `--wb greyworld` option. 129 | 130 | Version 1.4.8 *(2019-07-17)* 131 | ----------------------------- 132 | 133 | - Fixed bug where picam hangs up when large difference in video image occurs (#128). 134 | 135 | Version 1.4.7 *(2018-09-17)* 136 | ----------------------------- 137 | 138 | - Fixed issue with the latest firmware. 139 | - Fixed compile error (#97). 140 | 141 | Version 1.4.6 *(2017-02-13)* 142 | ----------------------------- 143 | 144 | - Added `--roi` option. 145 | 146 | Version 1.4.5 *(2017-01-01)* 147 | ----------------------------- 148 | 149 | - Fixed bug where `--evcomp` does not work properly. 150 | 151 | Version 1.4.4 *(2016-10-13)* 152 | ----------------------------- 153 | 154 | - Added `--ex` option. 155 | 156 | Version 1.4.3 *(2016-09-05)* 157 | ----------------------------- 158 | 159 | - Display texts in preview (by @nalajcie) 160 | - bugfix: Memory leak when using --time option 161 | 162 | Version 1.4.2 *(2016-07-24)* 163 | ----------------------------- 164 | 165 | - Add tab_scale directive to hooks/subtitle 166 | - Fix stream name for --rtspout option 167 | - bugfix: picam does not generate frames in VFR 168 | - Improve build settings and instructions (by @Linkaan) 169 | - Add option to set backgroud color for the preview (by @nalajcie) 170 | - Other small fixes 171 | 172 | Version 1.4.1 *(2015-11-26)* 173 | ----------------------------- 174 | 175 | - Add `--wbred` and `--wbblue` options. 176 | - Add wbred and wbblue hooks. 177 | 178 | Version 1.4.0 *(2015-11-23)* 179 | ----------------------------- 180 | 181 | ### Major changes 182 | 183 | - Add timestamp feature. 184 | - Add subtitle feature. 185 | - Introduce concepts of global and per-recording recordbuf. 186 | - Now it is able to specify the directory and filename for each recording. 187 | 188 | ### Minor changes 189 | 190 | - Set state/record to false after the recording is actually complete. 191 | - Change the format of state/*.ts files. 192 | 193 | Version 1.3.3 *(2015-10-06)* 194 | ----------------------------- 195 | 196 | - Add audio preview feature (`--audiopreview` and `--audiopreviewdev` options). 197 | - Change default value of `--opacity` to 0. 198 | 199 | Version 1.3.2 *(2015-09-01)* 200 | ----------------------------- 201 | 202 | - Add `--opacity` option. 203 | 204 | Version 1.3.1 *(2015-08-05)* 205 | ----------------------------- 206 | 207 | - Show error when picam receives an invalid hook 208 | - Fix white balance control for the latest firmware 209 | - Add hooks for white balance control 210 | 211 | Version 1.3.0 *(2015-04-04)* 212 | ----------------------------- 213 | 214 | - Add support for various AVC profiles and levels (--avcprofile, --avclevel). 215 | - Add support for 1080p (1920x1080) resolution. 216 | 217 | Version 1.2.10 *(2015-03-25)* 218 | ----------------------------- 219 | 220 | - Add option to control white balance (`--wb`). 221 | - Add options to control exposure (`--metering`, `--evcomp`, `--shutter`, `--iso`). 222 | - Fix: Generate correct PTS and DTS in variable frame rate mode. 223 | - Change protocol used in `--rtspout` to work with node-rtsp-rtmp-server. 224 | 225 | 226 | Version 1.2.9 *(2015-03-01)* 227 | ---------------------------- 228 | 229 | - Change PTS calculation for the latest Raspberry Pi firmware. 230 | - Remove `--ptsstep` option. 231 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | # set the project name 4 | project(picam) 5 | 6 | if (NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt) 7 | if (NOT CMAKE_BUILD_TYPE) 8 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) 9 | message(STATUS "No previous build - default to Release build") 10 | endif() 11 | endif() 12 | 13 | set(CMAKE_CXX_FLAGS_DEBUG "-g") 14 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 15 | 16 | set (CMAKE_EXPORT_COMPILE_COMMANDS ON) 17 | set (CMAKE_CXX_STANDARD 17) 18 | #add_compile_options(-Wall -Wextra -pedantic -Wno-unused-parameter -faligned-new -Werror -Wfatal-errors) 19 | add_compile_options(-Wall -Wextra -pedantic -Wno-unused-parameter -Werror -Wfatal-errors) 20 | add_definitions(-D_FILE_OFFSET_BITS=64) 21 | 22 | if (CMAKE_COMPILER_IS_GNUCXX) 23 | add_compile_options(-Wno-psabi) 24 | endif() 25 | 26 | option(BUILD_SHARED_LIBS "Build using shared libraries" ON) 27 | 28 | IF (NOT ENABLE_COMPILE_FLAGS_FOR_TARGET) 29 | # On a Pi this will give us armhf or arm64. 30 | execute_process(COMMAND dpkg-architecture -qDEB_HOST_ARCH 31 | OUTPUT_VARIABLE ENABLE_COMPILE_FLAGS_FOR_TARGET OUTPUT_STRIP_TRAILING_WHITESPACE) 32 | endif() 33 | message(STATUS "Platform: ${ENABLE_COMPILE_FLAGS_FOR_TARGET}") 34 | if ("${ENABLE_COMPILE_FLAGS_FOR_TARGET}" STREQUAL "arm64") 35 | # 64-bit binaries can be fully optimised. 36 | add_definitions(-ftree-vectorize) 37 | elseif ("${ENABLE_COMPILE_FLAGS_FOR_TARGET}" STREQUAL "armv8-neon") 38 | # Only build with 32-bit Pi 3/4 specific optimisations if requested on the command line. 39 | add_definitions(-mfpu=neon-fp-armv8 -ftree-vectorize) 40 | endif() 41 | 42 | # Source package generation setup. 43 | set(CPACK_GENERATOR "TXZ") 44 | set(CPACK_PACKAGE_FILE_NAME "libcamera-apps-build") 45 | set(CPACK_SOURCE_GENERATOR "TXZ") 46 | set(CPACK_INSTALL_SCRIPTS ${CMAKE_SOURCE_DIR}/package.cmake) 47 | set(CPACK_SOURCE_PACKAGE_FILE_NAME "libcamera-apps-src") 48 | set(CPACK_SOURCE_IGNORE_FILES "/\.git*;/build;") 49 | include(CPack) 50 | 51 | find_package(PkgConfig REQUIRED) 52 | 53 | pkg_check_modules(LIBCAMERA REQUIRED libcamera) 54 | message(STATUS "libcamera library found:") 55 | message(STATUS " version: ${LIBCAMERA_VERSION}") 56 | message(STATUS " libraries: ${LIBCAMERA_LINK_LIBRARIES}") 57 | message(STATUS " include path: ${LIBCAMERA_INCLUDE_DIRS}") 58 | include_directories(${CMAKE_SOURCE_DIR} ${LIBCAMERA_INCLUDE_DIRS} /usr/include/freetype2 /usr/include/harfbuzz /usr/include/fontconfig) 59 | 60 | #add_subdirectory(core) 61 | add_subdirectory(video_encoder) 62 | add_subdirectory(preview) 63 | add_subdirectory(text) 64 | add_subdirectory(subtitle) 65 | add_subdirectory(timestamp) 66 | add_subdirectory(log) 67 | add_subdirectory(httplivestreaming) 68 | add_subdirectory(mpegts) 69 | add_subdirectory(audio) 70 | add_subdirectory(muxer) 71 | add_subdirectory(libstate) 72 | add_subdirectory(libhook) 73 | add_subdirectory(picam_option) 74 | add_subdirectory(libpicam) 75 | #add_subdirectory(dispmanx) 76 | add_subdirectory(rtsp) 77 | -------------------------------------------------------------------------------- /audio/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(audio STATIC audio.cpp) 6 | # If asound is not linked, snd_pcm_hw_params_get_rate() will silently fail 7 | target_link_libraries(audio asound) 8 | 9 | install(TARGETS audio LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 10 | -------------------------------------------------------------------------------- /audio/audio.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | #include 7 | #include 8 | #include "httplivestreaming/httplivestreaming.h" 9 | #include "muxer/muxer.hpp" 10 | #include "picam_option/picam_option.hpp" 11 | 12 | class Audio 13 | { 14 | public: 15 | Audio(PicamOption *option); 16 | ~Audio(); 17 | void setup(HTTPLiveStreaming *hls); 18 | void loop(); 19 | void stop(); 20 | void teardown(); 21 | int read_audio_poll_mmap(); 22 | int wait_for_poll(snd_pcm_t *device, struct pollfd *target_fds, unsigned int audio_fd_count); 23 | void set_encode_callback(std::function callback); 24 | float get_fps(); 25 | void mute(); 26 | void unmute(); 27 | void set_audio_start_time(int64_t audio_start_time); 28 | void preconfigure(); 29 | 30 | protected: 31 | PicamOption *option; 32 | 33 | private: 34 | std::function encode_callback; 35 | bool keepRunning = true; 36 | int64_t audio_start_time = LLONG_MIN; 37 | int open_audio_capture_device(); 38 | int open_audio_preview_device(); 39 | void teardown_audio_capture_device(); 40 | void teardown_audio_preview_device(); 41 | void encode_and_send_audio(); 42 | void preconfigure_microphone(); 43 | int microphone_channels = -1; 44 | int get_audio_channels(); 45 | int get_audio_sample_rate(); 46 | int configure_audio_capture_device(); 47 | int64_t get_next_audio_write_time(); 48 | long long audio_frame_count = 0; 49 | int is_audio_preview_device_opened = 0; 50 | uint16_t *samples; 51 | void setup_av_frame(AVFormatContext *format_ctx); 52 | HTTPLiveStreaming *hls; 53 | bool is_muted = false; 54 | uint16_t ng_thresh = 0; 55 | float ng_attack_rate; 56 | float ng_release_rate; 57 | int ng_hold_samples; 58 | bool ng_open = false; 59 | float ng_attenuation = 0.0f; 60 | int ng_held_samples; 61 | }; 62 | -------------------------------------------------------------------------------- /core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | #find_package(Boost REQUIRED COMPONENTS program_options) 6 | 7 | #add_custom_target(VersionCpp ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -P ${CMAKE_CURRENT_LIST_DIR}/version.cmake) 8 | #set_source_files_properties(version.cpp PROPERTIES GENERATED 1) 9 | 10 | #add_library(libcamera_app libcamera_app.cpp post_processor.cpp version.cpp options.cpp) 11 | #add_dependencies(libcamera_app VersionCpp) 12 | 13 | #set_target_properties(libcamera_app PROPERTIES PREFIX "" IMPORT_PREFIX "") 14 | #target_link_libraries(libcamera_app pthread preview ${LIBCAMERA_LINK_LIBRARIES} ${Boost_LIBRARIES} post_processing_stages) 15 | 16 | #install(TARGETS libcamera_app LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 17 | -------------------------------------------------------------------------------- /core/completed_request.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2021, Raspberry Pi (Trading) Ltd. 4 | * 5 | * completed_request.hpp - structure holding request results. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include "core/metadata.hpp" 16 | 17 | struct CompletedRequest 18 | { 19 | using BufferMap = libcamera::Request::BufferMap; 20 | using ControlList = libcamera::ControlList; 21 | using Request = libcamera::Request; 22 | 23 | CompletedRequest(unsigned int seq, Request *r) 24 | : sequence(seq), buffers(r->buffers()), metadata(r->metadata()), request(r) 25 | { 26 | r->reuse(); 27 | } 28 | unsigned int sequence; 29 | BufferMap buffers; 30 | ControlList metadata; 31 | Request *request; 32 | float framerate; 33 | Metadata post_process_metadata; 34 | }; 35 | 36 | using CompletedRequestPtr = std::shared_ptr; 37 | -------------------------------------------------------------------------------- /core/frame_info.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2021, Raspberry Pi (Trading) Ltd. 4 | * 5 | * frame_info.hpp - Frame info class for libcamera apps 6 | */ 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | struct FrameInfo 16 | { 17 | FrameInfo(libcamera::ControlList &ctrls) 18 | : exposure_time(0.0), digital_gain(0.0), colour_gains({ { 0.0f, 0.0f } }), focus(0.0), aelock(false) 19 | { 20 | auto exp = ctrls.get(libcamera::controls::ExposureTime); 21 | if (exp) 22 | exposure_time = *exp; 23 | 24 | auto ag = ctrls.get(libcamera::controls::AnalogueGain); 25 | if (ag) 26 | analogue_gain = *ag; 27 | 28 | auto dg = ctrls.get(libcamera::controls::DigitalGain); 29 | if (dg) 30 | digital_gain = *dg; 31 | 32 | auto cg = ctrls.get(libcamera::controls::ColourGains); 33 | if (cg) 34 | { 35 | colour_gains[0] = (*cg)[0], colour_gains[1] = (*cg)[1]; 36 | } 37 | 38 | auto fom = ctrls.get(libcamera::controls::FocusFoM); 39 | if (fom) 40 | focus = *fom; 41 | 42 | auto ae = ctrls.get(libcamera::controls::AeLocked); 43 | if (ae) 44 | aelock = *ae; 45 | } 46 | 47 | std::string ToString(std::string &info_string) const 48 | { 49 | std::string parsed(info_string); 50 | 51 | for (auto const &t : tokens) 52 | { 53 | std::size_t pos = parsed.find(t); 54 | if (pos != std::string::npos) 55 | { 56 | std::stringstream value; 57 | value << std::fixed << std::setprecision(2); 58 | 59 | if (t == "%frame") 60 | value << sequence; 61 | else if (t == "%fps") 62 | value << fps; 63 | else if (t == "%exp") 64 | value << exposure_time; 65 | else if (t == "%ag") 66 | value << analogue_gain; 67 | else if (t == "%dg") 68 | value << digital_gain; 69 | else if (t == "%rg") 70 | value << colour_gains[0]; 71 | else if (t == "%bg") 72 | value << colour_gains[1]; 73 | else if (t == "%focus") 74 | value << focus; 75 | else if (t == "%aelock") 76 | value << aelock; 77 | 78 | parsed.replace(pos, t.length(), value.str()); 79 | } 80 | } 81 | 82 | return parsed; 83 | } 84 | 85 | unsigned int sequence; 86 | float exposure_time; 87 | float analogue_gain; 88 | float digital_gain; 89 | std::array colour_gains; 90 | float focus; 91 | float fps; 92 | bool aelock; 93 | 94 | private: 95 | // Info text tokens. 96 | inline static const std::string tokens[] = { "%frame", "%fps", "%exp", "%ag", "%dg", 97 | "%rg", "%bg", "%focus", "%aelock" }; 98 | }; 99 | -------------------------------------------------------------------------------- /core/metadata.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2019-2021, Raspberry Pi (Trading) Limited 4 | * 5 | * metadata.hpp - general metadata class 6 | */ 7 | #pragma once 8 | 9 | // A simple class for carrying arbitrary metadata, for example about an image. 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class Metadata 17 | { 18 | public: 19 | Metadata() = default; 20 | 21 | Metadata(Metadata const &other) 22 | { 23 | std::scoped_lock other_lock(other.mutex_); 24 | data_ = other.data_; 25 | } 26 | 27 | Metadata(Metadata &&other) 28 | { 29 | std::scoped_lock other_lock(other.mutex_); 30 | data_ = std::move(other.data_); 31 | other.data_.clear(); 32 | } 33 | 34 | template 35 | void Set(std::string const &tag, T &&value) 36 | { 37 | std::scoped_lock lock(mutex_); 38 | data_.insert_or_assign(tag, std::forward(value)); 39 | } 40 | 41 | template 42 | int Get(std::string const &tag, T &value) const 43 | { 44 | std::scoped_lock lock(mutex_); 45 | auto it = data_.find(tag); 46 | if (it == data_.end()) 47 | return -1; 48 | value = std::any_cast(it->second); 49 | return 0; 50 | } 51 | 52 | void Clear() 53 | { 54 | std::scoped_lock lock(mutex_); 55 | data_.clear(); 56 | } 57 | 58 | Metadata &operator=(Metadata const &other) 59 | { 60 | std::scoped_lock lock(mutex_, other.mutex_); 61 | data_ = other.data_; 62 | return *this; 63 | } 64 | 65 | Metadata &operator=(Metadata &&other) 66 | { 67 | std::scoped_lock lock(mutex_, other.mutex_); 68 | data_ = std::move(other.data_); 69 | other.data_.clear(); 70 | return *this; 71 | } 72 | 73 | void Merge(Metadata &other) 74 | { 75 | std::scoped_lock lock(mutex_, other.mutex_); 76 | data_.merge(other.data_); 77 | } 78 | 79 | template 80 | T *GetLocked(std::string const &tag) 81 | { 82 | // This allows in-place access to the Metadata contents, 83 | // for which you should be holding the lock. 84 | auto it = data_.find(tag); 85 | if (it == data_.end()) 86 | return nullptr; 87 | return std::any_cast(&it->second); 88 | } 89 | 90 | template 91 | void SetLocked(std::string const &tag, T &&value) 92 | { 93 | // Use this only if you're holding the lock yourself. 94 | data_.insert_or_assign(tag, std::forward(value)); 95 | } 96 | 97 | // Note: use of (lowercase) lock and unlock means you can create scoped 98 | // locks with the standard lock classes. 99 | // e.g. std::lock_guard lock(metadata) 100 | void lock() { mutex_.lock(); } 101 | void unlock() { mutex_.unlock(); } 102 | 103 | private: 104 | mutable std::mutex mutex_; 105 | std::map data_; 106 | }; 107 | -------------------------------------------------------------------------------- /core/stream_info.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2021, Raspberry Pi (Trading) Ltd. 4 | * 5 | * stream_info.hpp - structure holding details about a libcamera Stream. 6 | */ 7 | #pragma once 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | struct StreamInfo 15 | { 16 | StreamInfo() : width(0), height(0), stride(0) {} 17 | unsigned int width; 18 | unsigned int height; 19 | unsigned int stride; 20 | libcamera::PixelFormat pixel_format; 21 | std::optional colour_space; 22 | }; 23 | -------------------------------------------------------------------------------- /dispmanx/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(dispmanx dispmanx.c) 6 | target_link_libraries(dispmanx) 7 | 8 | install(TARGETS dispmanx LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | -------------------------------------------------------------------------------- /dispmanx/dispmanx.c: -------------------------------------------------------------------------------- 1 | #include "dispmanx.h" 2 | #include "log/log.h" 3 | #include "text/text.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | // for debugging: write TEST DATA at startup 11 | // and semi-transparet overlay each refresh to see all th texts overlayed 12 | //#define DEBUG_FILL_TEXT_OVERLAY 13 | 14 | 15 | static DISPMANX_DISPLAY_HANDLE_T g_display; 16 | static DISPMANX_MODEINFO_T g_modeInfo; 17 | static DISPMANX_ELEMENT_HANDLE_T g_bgElement; 18 | static DISPMANX_RESOURCE_HANDLE_T g_bgResource; 19 | 20 | static DISPMANX_ELEMENT_HANDLE_T g_textElement; 21 | static DISPMANX_RESOURCE_HANDLE_T g_frontResource; 22 | static DISPMANX_RESOURCE_HANDLE_T g_backResource; 23 | 24 | static uint8_t* g_canvas; 25 | static uint32_t g_canvas_size; 26 | static uint32_t g_canvas_height; 27 | static uint32_t g_canvas_width; 28 | 29 | static uint32_t g_video_width; 30 | static uint32_t g_video_height; 31 | 32 | #define DISP_CANVAS_BYTES_PER_PIXEL 4 33 | 34 | #ifdef DEBUG_FILL_TEXT_OVERLAY 35 | // just for testing: works only with ARGB images 36 | static void fill_rect(VC_IMAGE_TYPE_T type, void *canvas, int width, int height, int x, int y, int w, int h, int val) 37 | { 38 | int row; 39 | int col; 40 | 41 | int pitch = width * DISP_CANVAS_BYTES_PER_PIXEL; 42 | uint32_t *line = (uint32_t*)((uint8_t *)(canvas) + ((y * width) + x) * DISP_CANVAS_BYTES_PER_PIXEL); 43 | 44 | for (row = 0; (row < h) && (y + row < height); ++row) { 45 | for (col = 0; (col < w) && (x + col < width); ++col) { 46 | line[col] = val; 47 | } 48 | line = (uint32_t*)((uint8_t *)line + pitch); 49 | } 50 | } 51 | #endif 52 | 53 | static void dispmanx_create_background(uint32_t argb) 54 | { 55 | VC_RECT_T dst_rect, src_rect; 56 | DISPMANX_UPDATE_HANDLE_T update; 57 | uint32_t vc_image_ptr; 58 | int ret; 59 | 60 | // if alpha is fully transparent then background has no effect 61 | if (!(argb & 0xff000000)) { 62 | log_debug("dispmanx_create_background: fully transparent not creating overlay\n"); 63 | return; 64 | } 65 | 66 | // background: we create a 1x1 black pixel image that is added to display just behind video 67 | g_bgResource = vc_dispmanx_resource_create(VC_IMAGE_ARGB8888, 1 /*width*/, 1 /*height*/, &vc_image_ptr); 68 | assert(g_bgResource); 69 | 70 | vc_dispmanx_rect_set( &dst_rect, 0, 0, 1, 1); 71 | 72 | ret = vc_dispmanx_resource_write_data(g_bgResource, VC_IMAGE_ARGB8888, sizeof(argb), &argb, &dst_rect); 73 | assert(ret == 0); 74 | 75 | vc_dispmanx_rect_set(&src_rect, 0, 0, 1<<16, 1<<16); 76 | vc_dispmanx_rect_set(&dst_rect, 0, 0, 0, 0); 77 | 78 | update = vc_dispmanx_update_start(0); 79 | assert(update); 80 | 81 | g_bgElement = vc_dispmanx_element_add(update, g_display, DISP_LAYER_BACKGROUD, &dst_rect, g_bgResource, &src_rect, 82 | DISPMANX_PROTECTION_NONE, NULL, NULL, DISPMANX_STEREOSCOPIC_MONO); 83 | assert(g_bgElement); 84 | 85 | ret = vc_dispmanx_update_submit_sync(update); 86 | assert(ret == 0); 87 | } 88 | 89 | static void dispmanx_create_text_overlay(void) 90 | { 91 | VC_RECT_T dst_rect, src_rect; 92 | DISPMANX_UPDATE_HANDLE_T update; 93 | uint32_t vc_image_ptr; 94 | int ret; 95 | 96 | int width = ALIGN_UP(g_video_width, 32); 97 | int height = ALIGN_UP(g_video_height, 16); 98 | int x = (g_modeInfo.width - width) / 2; 99 | int y = (g_modeInfo.height - height) / 2; 100 | 101 | #if 0 102 | VC_DISPMANX_ALPHA_T alpha = { DISPMANX_FLAGS_ALPHA_FROM_SOURCE | DISPMANX_FLAGS_ALPHA_FIXED_ALL_PIXELS, 103 | 255, /*alpha 0->255*/ 104 | 0 }; 105 | #endif 106 | 107 | // we will be using double buffering - we're creating two resources with the size of the screen 108 | g_frontResource = vc_dispmanx_resource_create(VC_IMAGE_ARGB8888, width, height, &vc_image_ptr); 109 | assert(g_frontResource); 110 | g_backResource = vc_dispmanx_resource_create(VC_IMAGE_ARGB8888, width, height, &vc_image_ptr); 111 | assert(g_backResource); 112 | 113 | g_canvas_height = height; 114 | g_canvas_width = width; 115 | int pitch = g_canvas_width * DISP_CANVAS_BYTES_PER_PIXEL; 116 | g_canvas_size = pitch * g_canvas_height; 117 | g_canvas = calloc(1, g_canvas_size); 118 | assert(g_canvas); 119 | 120 | #ifdef DEBUG_FILL_TEXT_OVERLAY 121 | // for debugging: write TEST DATA 122 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 0, 0, 50, 50, 0xFFFFFFFF); 123 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 100, 100, 200, 200, 0xFFFF0000); 124 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 1100, 600, 500, 200, 0xFF0000FF); 125 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 150, 150, 200, 200, 0xFF00FF00); 126 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 500, 500, 200, 200, 0x8800FF00); 127 | 128 | vc_dispmanx_rect_set(&dst_rect, 0, 0, width, height); 129 | ret = vc_dispmanx_resource_write_data(g_frontResource, VC_IMAGE_ARGB8888, pitch, g_canvas, &dst_rect); 130 | assert(ret == 0); 131 | #endif 132 | 133 | vc_dispmanx_rect_set(&src_rect, 0, 0, width << 16, height << 16); 134 | vc_dispmanx_rect_set(&dst_rect, x, y, width, height); 135 | 136 | update = vc_dispmanx_update_start(0); 137 | assert(update); 138 | 139 | g_textElement = vc_dispmanx_element_add(update, g_display, DISP_LAYER_TEXT, &dst_rect, g_frontResource, &src_rect, 140 | DISPMANX_PROTECTION_NONE, 0 /*&alpha*/, NULL, DISPMANX_STEREOSCOPIC_MONO); 141 | assert(g_textElement); 142 | 143 | ret = vc_dispmanx_update_submit_sync(update); 144 | assert(ret == 0); 145 | log_debug("dispmanx: text overlay created!\n"); 146 | } 147 | 148 | void dispmanx_init(uint32_t bg_color, uint32_t video_width, uint32_t video_height) 149 | { 150 | int ret; 151 | log_debug("dispmanx: init\n"); 152 | g_display = vc_dispmanx_display_open(DISP_DISPLAY_DEFAULT); 153 | assert(g_display); 154 | 155 | g_video_width = video_width; 156 | g_video_height = video_height; 157 | ret = vc_dispmanx_display_get_info(g_display, &g_modeInfo); 158 | assert(ret == 0); 159 | log_info("dispmanx: display %d: %d x %d (video: %d x %d) \n", DISP_DISPLAY_DEFAULT, 160 | g_modeInfo.width, g_modeInfo.height, g_video_width, g_video_height); 161 | 162 | dispmanx_create_background(bg_color); 163 | dispmanx_create_text_overlay(); 164 | } 165 | 166 | #define ELEMENT_REMOVE_IF_EXISTS(_elem) do { if (_elem) { ret = vc_dispmanx_element_remove(update, _elem); assert(ret == 0); } } while(0) 167 | #define RESOURCE_DELETE_IF_EXISTS(_res) do { if (_res) { ret = vc_dispmanx_resource_delete(_res); assert(ret == 0); } } while(0) 168 | 169 | void dispmanx_destroy(void) 170 | { 171 | DISPMANX_UPDATE_HANDLE_T update; 172 | int ret; 173 | 174 | free(g_canvas); 175 | 176 | log_debug("dispmanx: destroy\n"); 177 | update = vc_dispmanx_update_start(0); 178 | assert(update != 0); 179 | 180 | ELEMENT_REMOVE_IF_EXISTS(g_bgElement); 181 | ELEMENT_REMOVE_IF_EXISTS(g_textElement); 182 | 183 | ret = vc_dispmanx_update_submit_sync(update); 184 | assert(ret == 0); 185 | 186 | RESOURCE_DELETE_IF_EXISTS(g_bgResource); 187 | RESOURCE_DELETE_IF_EXISTS(g_frontResource); 188 | RESOURCE_DELETE_IF_EXISTS(g_backResource); 189 | 190 | ret = vc_dispmanx_display_close(g_display); 191 | assert(ret == 0); 192 | } 193 | 194 | void dispmanx_update_text_overlay(void) 195 | { 196 | VC_RECT_T dst_rect; 197 | int ret; 198 | //clock_t start_time = clock(); 199 | 200 | 201 | // reset overlay to fully-transparent 202 | memset(g_canvas, 0, g_canvas_size); 203 | #ifdef DEBUG_FILL_TEXT_OVERLAY // really nice: see refresehed areas (layout boxes) 204 | fill_rect(VC_IMAGE_ARGB8888, g_canvas, g_canvas_width, g_canvas_height, 0, 0, g_canvas_width, g_canvas_height, 0x33ff0000); 205 | #endif 206 | 207 | 208 | // render texts 209 | text_draw_all(g_canvas, g_canvas_width, g_canvas_height, 0); // is_video = 0 210 | 211 | // write data to back resource 212 | vc_dispmanx_rect_set(&dst_rect, 0, 0, g_canvas_width, g_canvas_height); 213 | int pitch = g_canvas_width * DISP_CANVAS_BYTES_PER_PIXEL; 214 | ret = vc_dispmanx_resource_write_data(g_backResource, VC_IMAGE_ARGB8888, pitch, g_canvas, &dst_rect); 215 | assert(ret == 0); 216 | 217 | // change the source of text overlay 218 | DISPMANX_UPDATE_HANDLE_T update = vc_dispmanx_update_start(0); 219 | assert(update != 0); 220 | 221 | ret = vc_dispmanx_element_change_source(update, g_textElement, g_backResource); 222 | 223 | assert(ret == 0); 224 | ret = vc_dispmanx_update_submit(update, NULL, NULL); 225 | assert(ret == 0); 226 | 227 | // back <-> front 228 | DISPMANX_RESOURCE_HANDLE_T tmpResource = g_frontResource; 229 | g_frontResource = g_backResource; 230 | g_backResource = tmpResource; 231 | //log_debug("dispmanx: update took %d ms\n", (clock() - start_time) * 1000 / CLOCKS_PER_SEC); 232 | } 233 | -------------------------------------------------------------------------------- /dispmanx/dispmanx.h: -------------------------------------------------------------------------------- 1 | #ifndef PICAM_DISPMANX_H 2 | #define PICAM_DISPMANX_H 3 | 4 | #include 5 | 6 | // dispmanx layers consts for displaying multiple accelerated overlays in preview 7 | #define DISP_LAYER_BACKGROUD 0xe 8 | #define DISP_LAYER_VIDEO_PREVIEW 0xf 9 | #define DISP_LAYER_TEXT 0x1f 10 | 11 | // default ARGB color for preview background (black) 12 | #define BLANK_BACKGROUND_DEFAULT 0xff000000 13 | 14 | // display to which we will output the preview overlays 15 | #define DISP_DISPLAY_DEFAULT 0 16 | 17 | void dispmanx_init(uint32_t bg_color, uint32_t video_width, uint32_t video_height); 18 | void dispmanx_destroy(void); 19 | void dispmanx_update_text_overlay(void); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /etc/README.md: -------------------------------------------------------------------------------- 1 | The files inside this directory can be used as init script. 2 | 3 | To install: 4 | 5 | $ sudo cp init.d/picam /etc/init.d/ 6 | $ sudo cp default/picam /etc/default/ 7 | 8 | Edit /etc/default/picam to match your environment. 9 | 10 | To start picam automatically on boot: 11 | 12 | $ sudo update-rc.d picam defaults 13 | 14 | To disable autostart: 15 | 16 | $ sudo update-rc.d picam remove 17 | 18 | To start picam manually: 19 | 20 | $ sudo service picam start 21 | 22 | To stop picam manually: 23 | 24 | $ sudo service picam stop 25 | -------------------------------------------------------------------------------- /etc/default/picam: -------------------------------------------------------------------------------- 1 | ## Change this to prevent picam from being started as a system service (for 2 | ## example, if you want to run it from a regular user account) 3 | START_PICAM=true 4 | 5 | ## absolute path to picam directory 6 | PICAM_DIR=/home/pi/picam 7 | 8 | ## absolute path to picam command 9 | PICAM_COMMAND=$PICAM_DIR/picam 10 | 11 | ## user:group to run picam 12 | UGID="pi:pi" 13 | 14 | ## pid file 15 | PIDFILE=/var/run/picam.pid 16 | 17 | ## video frame rate 18 | VIDEO_FRAMERATE=30 19 | 20 | ## image width in pixels 21 | VIDEO_WIDTH=1280 22 | 23 | ## image height in pixels 24 | VIDEO_HEIGHT=720 25 | 26 | ## flip image horizontally 27 | VIDEO_HORIZONTAL_FLIP=false 28 | 29 | ## flip image vertically 30 | VIDEO_VERTICAL_FLIP=false 31 | 32 | ## video stream bitrate in bits/s 33 | VIDEO_BITRATE=2000000 34 | 35 | ## alsa device name 36 | ALSADEV="hw:0,0" 37 | 38 | ## enable audio recording 39 | ENABLE_AUDIO=true 40 | 41 | ## enable HTTP Live Streaming (HLS) 42 | ENABLE_HLS=false 43 | 44 | ## directory to output HLS files 45 | HLS_DIR="/run/shm/hls" 46 | 47 | ## enable encryption 48 | ENABLE_HLS_ENCRYPTION=true 49 | 50 | ## encryption key file 51 | HLS_ENCRYPTION_KEY_FILE="enc.key" 52 | 53 | ## create encryption key file in $HLS_DIR on startup 54 | HLS_CREATE_ENCRYPTION_KEY_FILE=true 55 | 56 | ## HLS encryption key (hexstring) 57 | HLS_ENCRYPTION_KEY="000102030405060708090a0b0c0d0e0f" 58 | 59 | ## HLS encryption initialization vector (hexstring) 60 | HLS_ENCRYPTION_IV="101112131415161718191a1b1c1d1e1f" 61 | 62 | ## other command line options 63 | OTHER_OPTIONS="" 64 | -------------------------------------------------------------------------------- /etc/init.d/picam: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: picam 4 | # Required-Start: $ALL 5 | # Required-Stop: $ALL 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: picam 9 | ### END INIT INFO 10 | 11 | # 12 | # Author: Emanuel Kuehnel 13 | # Nao Iizuka 14 | # 15 | 16 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 17 | DESC="picam" 18 | NAME="picam" 19 | PICAM_DIR="/home/pi/picam" 20 | PICAM_COMMAND="$PICAM_DIR/picam" 21 | UGID="pi:pi" 22 | SCRIPTNAME=/etc/init.d/$NAME 23 | START_PICAM=false 24 | CONFIG_FILE=/etc/default/$NAME 25 | 26 | # Read configuration variable file if it is present 27 | [ -r $CONFIG_FILE ] && . $CONFIG_FILE 28 | 29 | # Load the VERBOSE setting and other rcS variables 30 | . /lib/init/vars.sh 31 | 32 | # Define LSB log_* functions. 33 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present 34 | # and status_of_proc is working. 35 | . /lib/lsb/init-functions 36 | 37 | # Check if $PICAM_DIR is accessible 38 | if [ ! -d "$PICAM_DIR" ] || [ ! -x "$PICAM_DIR" ]; then 39 | echo "Error: $PICAM_DIR is not accessible" 40 | echo "Hint: Set PICAM_DIR in $CONFIG_FILE" 41 | exit 1 42 | fi 43 | 44 | # Compose command line options 45 | PICAM_OPTIONS="--quiet \ 46 | --fps $VIDEO_FRAMERATE \ 47 | --width $VIDEO_WIDTH \ 48 | --height $VIDEO_HEIGHT \ 49 | --videobitrate $VIDEO_BITRATE \ 50 | --alsadev $ALSADEV \ 51 | $OTHER_OPTIONS" 52 | if [ "$ENABLE_HLS" = "true" ]; then 53 | PICAM_OPTIONS="$PICAM_OPTIONS --hlsdir $HLS_DIR" 54 | if [ "$ENABLE_HLS_ENCRYPTION" = "true" ]; then 55 | PICAM_OPTIONS="$PICAM_OPTIONS --hlsenc \ 56 | --hlsenckeyuri $HLS_ENCRYPTION_KEY_FILE \ 57 | --hlsenckey $HLS_ENCRYPTION_KEY \ 58 | --hlsenciv $HLS_ENCRYPTION_IV" 59 | fi 60 | fi 61 | if [ "$VIDEO_HORIZONTAL_FLIP" = "true" ]; then 62 | PICAM_OPTIONS="$PICAM_OPTIONS --hflip" 63 | fi 64 | if [ "$VIDEO_VERTICAL_FLIP" = "true" ]; then 65 | PICAM_OPTIONS="$PICAM_OPTIONS --vflip" 66 | fi 67 | if [ "$ENABLE_AUDIO" = "false" ]; then 68 | PICAM_OPTIONS="$PICAM_OPTIONS --noaudio" 69 | fi 70 | 71 | # 72 | # Function that starts the daemon/service 73 | # 74 | do_start() 75 | { 76 | if [ $START_PICAM = "true" ]; then 77 | # Create hooks dir and set permission 78 | if [ ! -d $PICAM_DIR/hooks ]; then 79 | echo "creating $PICAM_DIR/hooks directory" 80 | mkdir -p $PICAM_DIR/hooks 81 | fi 82 | if [ -d $PICAM_DIR/hooks ]; then 83 | chown -R $UGID $PICAM_DIR/hooks 84 | else 85 | echo "Error: failed to create $PICAM_DIR/hooks" 86 | exit 1 87 | fi 88 | 89 | # Create state dir and set permission 90 | if [ ! -d $PICAM_DIR/state ]; then 91 | echo "creating $PICAM_DIR/state directory" 92 | mkdir -p $PICAM_DIR/state 93 | fi 94 | if [ -d $PICAM_DIR/state ]; then 95 | chown -R $UGID $PICAM_DIR/state 96 | else 97 | echo "Error: failed to create $PICAM_DIR/state" 98 | exit 1 99 | fi 100 | 101 | # Create rec dir and set permission 102 | if [ ! -d $PICAM_DIR/rec ]; then 103 | echo "creating $PICAM_DIR/rec directory" 104 | mkdir -p $PICAM_DIR/rec 105 | fi 106 | if [ -d $PICAM_DIR/rec ]; then 107 | chown -R $UGID $PICAM_DIR/rec 108 | else 109 | echo "Error: failed to create $PICAM_DIR/rec" 110 | exit 1 111 | fi 112 | 113 | if [ "$ENABLE_HLS" = "true" ]; then 114 | if [ ! -d $HLS_DIR ]; then 115 | echo "creating $HLS_DIR directory" 116 | mkdir -p $HLS_DIR 117 | fi 118 | # Correct permissions for $HLS_DIR 119 | if [ -d $HLS_DIR ]; then 120 | chown -R $UGID $HLS_DIR 121 | else 122 | echo "Error: failed to create $HLS_DIR" 123 | exit 1 124 | fi 125 | 126 | if [ "$ENABLE_HLS_ENCRYPTION" = "true" ] && [ "$HLS_CREATE_ENCRYPTION_KEY_FILE" = "true" ]; then 127 | # Create encryption key file 128 | printf "$HLS_ENCRYPTION_KEY" | \ 129 | perl -ne 'print pack "H*", $_' > $HLS_DIR"/"$HLS_ENCRYPTION_KEY_FILE 130 | fi 131 | [ -f $HLS_DIR"/"$HLS_ENCRYPTION_KEY_FILE ] && chown $UGID $HLS_DIR"/"$HLS_ENCRYPTION_KEY_FILE 132 | fi 133 | 134 | # Exit if picam is not executable 135 | [ -x "$PICAM_COMMAND" ] || { 136 | echo "Error: $PICAM_COMMAND is not executable" 137 | echo "Hint: Set PICAM_COMMAND in $CONFIG_FILE" 138 | exit 1 139 | } 140 | 141 | # Return 142 | # 0 if daemon has been started 143 | # 1 if daemon was already running 144 | # 2 if daemon could not be started 145 | start-stop-daemon \ 146 | --start \ 147 | --pidfile $PIDFILE \ 148 | --chuid $UGID \ 149 | --exec $PICAM_COMMAND \ 150 | --test \ 151 | > /dev/null \ 152 | || return 1 153 | ( start-stop-daemon \ 154 | --start \ 155 | --background \ 156 | --make-pidfile \ 157 | --pidfile $PIDFILE \ 158 | --chuid $UGID \ 159 | --chdir $PICAM_DIR \ 160 | --exec $PICAM_COMMAND \ 161 | -- $PICAM_OPTIONS ) \ 162 | || return 2 163 | else 164 | echo "service disabled in $CONFIG_FILE" 165 | fi 166 | } 167 | 168 | # 169 | # Function that stops the daemon/service 170 | # 171 | do_stop() 172 | { 173 | # Return 174 | # 0 if daemon has been stopped 175 | # 1 if daemon was already stopped 176 | # 2 if daemon could not be stopped 177 | # other if a failure occurred 178 | start-stop-daemon --stop --retry=TERM/10/KILL/5 --pidfile $PIDFILE --name $NAME 179 | RETVAL="$?" 180 | [ "$RETVAL" = 2 ] && return 2 181 | # Many daemons don't delete their pidfiles when they exit. 182 | [ -f $PIDFILE ] && rm -f $PIDFILE 183 | 184 | # Delete HLS files 185 | if [ "$ENABLE_HLS" = "true" ]; then 186 | find $HLS_DIR -name *.ts -exec rm {} \; 187 | [ -f $HLS_DIR/index.m3u8 ] && rm $HLS_DIR/index.m3u8 188 | if [ "$HLS_CREATE_ENCRYPTION_KEY_FILE" = "true" ]; then 189 | [ -f $HLS_DIR/$HLS_ENCRYPTION_KEY_FILE ] && rm $HLS_DIR/$HLS_ENCRYPTION_KEY_FILE 190 | fi 191 | fi 192 | 193 | return "$RETVAL" 194 | } 195 | 196 | case "$1" in 197 | start) 198 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 199 | do_start 200 | case "$?" in 201 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 202 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 203 | esac 204 | ;; 205 | stop) 206 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 207 | do_stop 208 | case "$?" in 209 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 210 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 211 | esac 212 | ;; 213 | status) 214 | status_of_proc "$PICAM_COMMAND" "$NAME" && exit 0 || exit $? 215 | ;; 216 | restart) 217 | # 218 | # If the "reload" option is implemented then remove the 219 | # 'force-reload' alias 220 | # 221 | log_daemon_msg "Restarting $DESC" "$NAME" 222 | do_stop 223 | case "$?" in 224 | 0|1) 225 | do_start 226 | case "$?" in 227 | 0) log_end_msg 0 ;; 228 | 1) log_end_msg 1 ;; # Old process is still running 229 | *) log_end_msg 1 ;; # Failed to start 230 | esac 231 | ;; 232 | *) 233 | # Failed to stop 234 | log_end_msg 1 235 | ;; 236 | esac 237 | ;; 238 | *) 239 | echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2 240 | exit 3 241 | ;; 242 | esac 243 | 244 | exit 0 245 | -------------------------------------------------------------------------------- /httplivestreaming/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(httplivestreaming STATIC httplivestreaming.c) 6 | target_link_libraries(httplivestreaming mpegts crypto) 7 | 8 | install(TARGETS httplivestreaming LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /httplivestreaming/httplivestreaming.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "httplivestreaming.h" 6 | #include "mpegts/mpegts.h" 7 | 8 | // Derived the typedefs from libavformat/mpegtsenc.c in FFmpeg source 9 | // because this is the only way to manipulate continuity counters. 10 | // Original license claim is as follows. 11 | /* 12 | * MPEG2 transport stream (aka DVB) muxer 13 | * Copyright (c) 2003 Fabrice Bellard 14 | * 15 | * This file is part of FFmpeg. 16 | * 17 | * FFmpeg is free software; you can redistribute it and/or 18 | * modify it under the terms of the GNU Lesser General Public 19 | * License as published by the Free Software Foundation; either 20 | * version 2.1 of the License, or (at your option) any later version. 21 | * 22 | * FFmpeg is distributed in the hope that it will be useful, 23 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 25 | * Lesser General Public License for more details. 26 | * 27 | * You should have received a copy of the GNU Lesser General Public 28 | * License along with FFmpeg; if not, write to the Free Software 29 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 30 | */ 31 | /** START OF COPY **/ 32 | typedef struct MpegTSSection { 33 | int pid; 34 | int cc; 35 | #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 57, 100) 36 | int discontinuity; 37 | #endif 38 | void (*write_packet)(struct MpegTSSection *s, const uint8_t *packet); 39 | void *opaque; 40 | } MpegTSSection; 41 | 42 | typedef struct MpegTSService { 43 | MpegTSSection pmt; /* MPEG-2 PMT table context */ 44 | // 45 | // NOTE: there are more fields but we don't need them 46 | // 47 | } MpegTSService; 48 | 49 | typedef struct MpegTSWrite { 50 | const AVClass *av_class; 51 | MpegTSSection pat; /* MPEG-2 PAT table */ 52 | MpegTSSection sdt; /* MPEG-2 SDT table context */ 53 | MpegTSSection nit; /* MPEG-2 NIT table context */ 54 | MpegTSService **services; 55 | AVPacket *pkt; 56 | int64_t sdt_period; /* SDT period in PCR time base */ 57 | int64_t pat_period; /* PAT/PMT period in PCR time base */ 58 | int64_t nit_period; /* NIT period in PCR time base */ 59 | int nb_services; 60 | // 61 | // NOTE: there are more fields but we don't need them 62 | // 63 | } MpegTSWrite; 64 | 65 | typedef struct MpegTSWriteStream { 66 | int pid; /* stream associated pid */ 67 | int cc; 68 | // 69 | // NOTE: there are more fields but we don't need them 70 | // 71 | } MpegTSWriteStream; 72 | /** END OF COPY **/ 73 | 74 | void encrypt_most_recent_file(HTTPLiveStreaming *hls) { 75 | char filepath[1024]; 76 | FILE *fp; 77 | uint8_t *input_data; 78 | uint8_t *encrypted_data; 79 | int input_size; 80 | EVP_CIPHER_CTX *enc_ctx; 81 | 82 | // init cipher context 83 | enc_ctx = EVP_CIPHER_CTX_new(); 84 | if (enc_ctx == NULL) { 85 | fprintf(stderr, "Error: encrypt_most_recent_file: EVP_CIPHER_CTX_new failed\n"); 86 | return; 87 | } 88 | 89 | if (hls->encryption_key == NULL) { 90 | fprintf(stderr, "Warning: encryption_key is not set\n"); 91 | } 92 | if (hls->encryption_iv == NULL) { 93 | fprintf(stderr, "Warning: encryption_iv is not set\n"); 94 | } 95 | EVP_EncryptInit_ex(enc_ctx, EVP_aes_128_cbc(), NULL, hls->encryption_key, hls->encryption_iv); 96 | 97 | // read original data 98 | snprintf(filepath, 1024, "%s/%d.ts", hls->dir, hls->most_recent_number); 99 | fp = fopen(filepath, "rb+"); 100 | if (fp == NULL) { 101 | perror("Read ts file failed"); 102 | return; 103 | } 104 | fseek(fp, 0, SEEK_END); 105 | input_size = ftell(fp); 106 | fseek(fp, 0, SEEK_SET); 107 | input_data = malloc(input_size); 108 | if (input_data == NULL) { 109 | perror("Can't malloc for input_data"); 110 | return; 111 | } 112 | fread(input_data, 1, input_size, fp); 113 | 114 | // encrypt the data 115 | int c_len = input_size + AES_BLOCK_SIZE; 116 | int f_len; 117 | int encrypted_size; 118 | encrypted_data = malloc(c_len); 119 | if (encrypted_data == NULL) { 120 | perror("Can't malloc for encrypted_data"); 121 | return; 122 | } 123 | EVP_EncryptUpdate(enc_ctx, encrypted_data, &c_len, input_data, input_size); 124 | EVP_EncryptFinal_ex(enc_ctx, encrypted_data+c_len, &f_len); 125 | encrypted_size = c_len + f_len; 126 | 127 | // write data to the same file 128 | fseek(fp, 0, SEEK_SET); 129 | fwrite(encrypted_data, 1, encrypted_size, fp); 130 | ftruncate(fileno(fp), encrypted_size); 131 | fclose(fp); 132 | 133 | // free up variables 134 | free(encrypted_data); 135 | free(input_data); 136 | EVP_CIPHER_CTX_free(enc_ctx); 137 | } 138 | 139 | static float max_float(float a, float b) { 140 | return (a >= b) ? a : b; 141 | } 142 | 143 | int calc_target_duration(HTTPLiveStreaming *hls) { 144 | float max = 0; 145 | int i; 146 | 147 | // segments 148 | int from_seq = hls->most_recent_number - hls->num_recent_files + 1; 149 | if (from_seq < 1) { 150 | from_seq = 1; 151 | } 152 | int num_segments = hls->most_recent_number - from_seq + 1; 153 | int segment_durations_idx = hls->segment_durations_idx - num_segments + 1; 154 | if (segment_durations_idx < 0) { 155 | segment_durations_idx += hls->num_recent_files; 156 | } 157 | for (i = 0; i < num_segments; i++) { 158 | max = max_float(max, hls->segment_durations[segment_durations_idx]); 159 | if (++segment_durations_idx == hls->num_recent_files) { 160 | segment_durations_idx = 0; 161 | } 162 | } 163 | 164 | // Round target duration to nearest integer 165 | return (int) (max + 0.5f); 166 | } 167 | 168 | // Write m3u8 file 169 | int write_index(HTTPLiveStreaming *hls, int is_end) { 170 | FILE *file; 171 | char buf[128]; 172 | char tmp_filepath[1024]; 173 | char filepath[1024]; 174 | int i; 175 | 176 | snprintf(tmp_filepath, 1024, "%s/_%s", hls->dir, hls->index_filename); 177 | file = fopen(tmp_filepath, "w"); 178 | if (!file) { 179 | perror("fopen"); 180 | return -1; 181 | } 182 | 183 | int target_duration = calc_target_duration(hls); 184 | // header 185 | // What features are available in each version can be found on: 186 | // https://developer.apple.com/library/ios/qa/qa1752/_index.html 187 | snprintf(buf, 128, "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:%d\n#EXT-X-MEDIA-SEQUENCE:%d\n#EXT-X-ALLOW-CACHE:NO\n", 188 | target_duration, hls->most_recent_number); 189 | fwrite(buf, 1, strlen(buf), file); 190 | 191 | // insert encryption header if needed 192 | if (hls->use_encryption) { 193 | if (hls->encryption_key_uri == NULL) { 194 | fprintf(stderr, "Error: encryption_key_uri is not set\n"); 195 | } else { 196 | snprintf(buf, 128, "#EXT-X-KEY:METHOD=AES-128,URI=\"%s\",IV=0x", 197 | hls->encryption_key_uri); 198 | fwrite(buf, 1, strlen(buf), file); 199 | for (i = 0; i < 16; i++) { 200 | snprintf(buf + i * 2, 3, "%02x", hls->encryption_iv[i]); 201 | } 202 | snprintf(buf + 32, 2, "\n"); 203 | fwrite(buf, 1, 33, file); 204 | } 205 | } 206 | 207 | // segments 208 | int from_seq = hls->most_recent_number - hls->num_recent_files + 1; 209 | if (from_seq < 1) { 210 | from_seq = 1; 211 | } 212 | int num_segments = hls->most_recent_number - from_seq + 1; 213 | int segment_durations_idx = hls->segment_durations_idx - num_segments + 1; 214 | if (segment_durations_idx < 0) { 215 | segment_durations_idx += hls->num_recent_files; 216 | } 217 | for (i = 0; i < num_segments; i++) { 218 | snprintf(buf, 128, "#EXTINF:%.5f,\n%d.ts\n", 219 | hls->segment_durations[segment_durations_idx], 220 | from_seq + i); 221 | fwrite(buf, 1, strlen(buf), file); 222 | if (++segment_durations_idx == hls->num_recent_files) { 223 | segment_durations_idx = 0; 224 | } 225 | } 226 | 227 | if (is_end) { 228 | // end mark 229 | fwrite("#EXT-X-ENDLIST\n", 1, 15, file); 230 | } 231 | 232 | fclose(file); 233 | 234 | snprintf(filepath, 1024, "%s/%s", hls->dir, hls->index_filename); 235 | rename(tmp_filepath, filepath); 236 | 237 | int last_seq = hls->most_recent_number - hls->num_recent_files - hls->num_retained_old_files; 238 | if (last_seq >= 1) { 239 | snprintf(filepath, 1024, "%s/%d.ts", hls->dir, last_seq); 240 | unlink(filepath); 241 | } 242 | 243 | return 0; 244 | } 245 | 246 | void hls_destroy(HTTPLiveStreaming *hls) { 247 | if (hls->is_started) { 248 | mpegts_close_stream(hls->format_ctx); 249 | if (hls->use_encryption) { 250 | encrypt_most_recent_file(hls); 251 | if (hls->encryption_key_uri != NULL) { 252 | free(hls->encryption_key_uri); 253 | } 254 | if (hls->encryption_key != NULL) { 255 | free(hls->encryption_key); 256 | } 257 | if (hls->encryption_iv != NULL) { 258 | free(hls->encryption_iv); 259 | } 260 | } 261 | 262 | if (++hls->segment_durations_idx == hls->num_recent_files) { 263 | hls->segment_durations_idx = 0; 264 | } 265 | hls->segment_durations[hls->segment_durations_idx] = 266 | (hls->last_packet_pts - hls->segment_start_pts) / 90000.0; 267 | 268 | write_index(hls, 1); 269 | } 270 | mpegts_destroy_context(hls->format_ctx); 271 | free(hls->segment_durations); 272 | free(hls); 273 | } 274 | 275 | void create_new_ts(HTTPLiveStreaming *hls) { 276 | char filepath[1024]; 277 | 278 | hls->most_recent_number++; 279 | snprintf(filepath, 1024, "%s/%d.ts", hls->dir, hls->most_recent_number); 280 | mpegts_open_stream(hls->format_ctx, filepath, 0); 281 | } 282 | 283 | int hls_write_packet(HTTPLiveStreaming *hls, AVPacket *pkt, int split) { 284 | MpegTSWrite *ts; 285 | MpegTSWriteStream *ts_st; 286 | uint8_t pat_cc; // holds continuity counter for program association table 287 | uint8_t sdt_cc; // holds continuity counter for service description table 288 | int *stream_cc; // holds continuity counter for streams 289 | int nb_streams; // holds the number of streams 290 | int *service_cc; // holds continuity counter for services 291 | int nb_services; // holds the number of services 292 | int i; 293 | 294 | if ( ! hls->is_started ) { 295 | hls->is_started = 1; 296 | create_new_ts(hls); 297 | hls->segment_start_pts = pkt->pts; 298 | hls->segment_durations_idx = 0; 299 | } 300 | 301 | if (split) { 302 | // Store the last segment duration 303 | if (++hls->segment_durations_idx == hls->num_recent_files) { 304 | hls->segment_durations_idx = 0; 305 | } 306 | hls->segment_durations[hls->segment_durations_idx] = 307 | (pkt->pts - hls->segment_start_pts) / 90000.0; 308 | hls->segment_start_pts = pkt->pts; 309 | 310 | // Flush remaining packets 311 | av_write_frame(hls->format_ctx, NULL); 312 | 313 | // Retain continuity counters 314 | ts = hls->format_ctx->priv_data; 315 | pat_cc = ts->pat.cc; // Get the continuity counter for Program Association Table 316 | sdt_cc = ts->sdt.cc; // Get the continuity counter for Service Description Table 317 | 318 | // Get the continuity counters for audio/video 319 | nb_streams = hls->format_ctx->nb_streams; 320 | stream_cc = malloc(sizeof(int) * nb_streams); 321 | if (stream_cc == NULL) { 322 | perror("malloc failed for stream_cc"); 323 | exit(EXIT_FAILURE); 324 | } 325 | for (i = 0; i < nb_streams; i++) { 326 | ts_st = hls->format_ctx->streams[i]->priv_data; 327 | stream_cc[i] = ts_st->cc; 328 | } 329 | 330 | // Get the continuity counter for Program Map Table 331 | nb_services = ts->nb_services; 332 | service_cc = malloc(sizeof(int) * nb_services); 333 | if (service_cc == NULL) { 334 | perror("malloc failed for service_cc"); 335 | exit(EXIT_FAILURE); 336 | } 337 | for (i = 0; i < nb_services; i++) { 338 | service_cc[i] = ts->services[i]->pmt.cc; 339 | } 340 | 341 | mpegts_close_stream(hls->format_ctx); 342 | if (hls->use_encryption) { 343 | encrypt_most_recent_file(hls); 344 | } 345 | write_index(hls, 0); 346 | create_new_ts(hls); 347 | 348 | // Restore continuity counters 349 | ts = hls->format_ctx->priv_data; 350 | ts->pat.cc = pat_cc; // Set the continuity counter for Program Association Table 351 | ts->sdt.cc = sdt_cc; // Set the continuity counter for Service Description Table 352 | 353 | // Set the continuity counters for audio/video 354 | for (i = 0; i < nb_streams; i++) { 355 | ts_st = hls->format_ctx->streams[i]->priv_data; 356 | ts_st->cc = stream_cc[i]; 357 | } 358 | free(stream_cc); 359 | 360 | // Set the continuity counter for Program Map Table 361 | if (ts->nb_services != nb_services) { 362 | fprintf(stderr, "warn: ts->nb_services (%d) != nb_services (%d)", ts->nb_services, nb_services); 363 | } 364 | for (i = 0; i < ts->nb_services && i < nb_services; i++) { 365 | ts->services[i]->pmt.cc = service_cc[i]; 366 | } 367 | free(service_cc); 368 | } 369 | 370 | if (hls->is_audio_only) { 371 | hls->last_packet_pts = pkt->pts; 372 | } else if (pkt->stream_index == 0) { // video frame 373 | hls->last_packet_pts = pkt->pts; 374 | } 375 | 376 | return av_write_frame(hls->format_ctx, pkt); 377 | } 378 | 379 | HTTPLiveStreaming *_hls_create(int num_recent_files, int is_audio_only, MpegTSCodecSettings *settings) { 380 | HTTPLiveStreaming *hls = malloc(sizeof(HTTPLiveStreaming)); 381 | if (hls == NULL) { 382 | perror("no memory for hls"); 383 | return NULL; 384 | } 385 | MpegTSContext mpegts_ctx; 386 | // HTTP Live Streaming does not allow video-only stream 387 | if (is_audio_only) { 388 | mpegts_ctx = mpegts_create_context_audio_only(settings); 389 | } else { 390 | mpegts_ctx = mpegts_create_context(settings); 391 | } 392 | AVFormatContext *format_ctx = mpegts_ctx.format_context; 393 | hls->audio_ctx = mpegts_ctx.codec_context_audio; 394 | hls->video_ctx = mpegts_ctx.codec_context_video; 395 | hls->is_audio_only = is_audio_only; 396 | hls->format_ctx = format_ctx; 397 | hls->index_filename = "index.m3u8"; 398 | hls->num_recent_files = num_recent_files; 399 | hls->num_retained_old_files = 10; 400 | hls->most_recent_number = 0; 401 | hls->dir = "."; 402 | hls->is_started = 0; 403 | hls->use_encryption = 0; 404 | hls->encryption_key_uri = NULL; 405 | hls->encryption_key = NULL; 406 | hls->encryption_iv = NULL; 407 | hls->segment_durations = malloc(sizeof(float) * num_recent_files); 408 | if (hls->segment_durations == NULL) { 409 | perror("no memory for hls->segment_durations"); 410 | free(hls); 411 | return NULL; 412 | } 413 | hls->segment_start_pts = 0; 414 | hls->last_packet_pts = 0; 415 | return hls; 416 | } 417 | 418 | HTTPLiveStreaming *hls_create(int num_recent_files, MpegTSCodecSettings *settings) { 419 | return _hls_create(num_recent_files, 0, settings); 420 | } 421 | 422 | HTTPLiveStreaming *hls_create_audio_only(int num_recent_files, MpegTSCodecSettings *settings) { 423 | return _hls_create(num_recent_files, 1, settings); 424 | } 425 | -------------------------------------------------------------------------------- /httplivestreaming/httplivestreaming.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef _CLIB_HTTPLIVESTREAMING_H_ 6 | #define _CLIB_HTTPLIVESTREAMING_H_ 7 | 8 | #include 9 | #include "mpegts/mpegts.h" 10 | 11 | typedef struct HTTPLiveStreaming { 12 | AVFormatContext *format_ctx; 13 | AVCodecContext *audio_ctx; 14 | AVCodecContext *video_ctx; 15 | char *index_filename; 16 | int num_recent_files; 17 | int num_retained_old_files; 18 | int most_recent_number; 19 | char *dir; 20 | int is_started; 21 | int use_encryption; 22 | char *encryption_key_uri; 23 | uint8_t *encryption_key; 24 | uint8_t *encryption_iv; 25 | int64_t segment_start_pts; 26 | int64_t last_packet_pts; 27 | float *segment_durations; 28 | int segment_durations_idx; 29 | int is_audio_only; 30 | } HTTPLiveStreaming; 31 | 32 | HTTPLiveStreaming *hls_create(int num_recent_files, MpegTSCodecSettings *settings); 33 | HTTPLiveStreaming *hls_create_audio_only(int num_recent_files, MpegTSCodecSettings *settings); 34 | int hls_write_packet(HTTPLiveStreaming *hls, AVPacket *pkt, int split); 35 | void hls_destroy(HTTPLiveStreaming *hls); 36 | 37 | #endif 38 | 39 | #ifdef __cplusplus 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /images/subtitle_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_example1.png -------------------------------------------------------------------------------- /images/subtitle_example1_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_example1_small.png -------------------------------------------------------------------------------- /images/subtitle_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_example2.png -------------------------------------------------------------------------------- /images/subtitle_example2_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_example2_small.png -------------------------------------------------------------------------------- /images/subtitle_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_intro.png -------------------------------------------------------------------------------- /images/subtitle_intro_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/subtitle_intro_small.png -------------------------------------------------------------------------------- /images/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/picam/a8cf94031727dcc37af51d73698dae5adae62695/images/youtube.png -------------------------------------------------------------------------------- /libhook/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(libhook STATIC hook.c) 6 | target_link_libraries(libhook) 7 | set_target_properties(libhook PROPERTIES PREFIX "") 8 | 9 | install(TARGETS libhook LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 10 | -------------------------------------------------------------------------------- /libhook/hook.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "hook.h" 14 | 15 | #define NUM_EVENT_BUF 10 16 | #define EVENT_NAME_BUF_LEN 32 17 | 18 | #define EVENT_SIZE ( sizeof (struct inotify_event) ) 19 | #define EVENT_BUF_LEN ( NUM_EVENT_BUF * ( EVENT_SIZE + EVENT_NAME_BUF_LEN ) ) 20 | 21 | typedef struct watch_target { 22 | char *dir; 23 | void (*callback)(char *, char *); 24 | int read_content; 25 | } watch_target; 26 | 27 | static int keep_watching = 1; 28 | static pthread_t *watcher_thread; 29 | 30 | void sig_handler(int signum) { 31 | // nop (since we just want to interrupt read()) 32 | } 33 | 34 | // Create hooks dir if it does not exist 35 | int hooks_create_dir(char *dir) { 36 | struct stat st; 37 | int err; 38 | 39 | err = stat(dir, &st); 40 | if (err == -1) { 41 | if (errno == ENOENT) { 42 | // Check if this is a broken symbolic link 43 | err = lstat(dir, &st); 44 | if (err == 0 && S_ISLNK(st.st_mode)) { 45 | fprintf(stderr, "error: ./%s is a broken symbolic link\n", dir); 46 | return -1; 47 | } 48 | 49 | // create directory 50 | if (mkdir(dir, 0755) == 0) { // success 51 | fprintf(stderr, "created hooks dir: ./%s\n", dir); 52 | } else { // error 53 | fprintf(stderr, "error creating hooks dir (./%s): %s\n", 54 | dir, strerror(errno)); 55 | return -1; 56 | } 57 | } else { 58 | perror("stat hooks dir"); 59 | return -1; 60 | } 61 | } else { 62 | if (!S_ISDIR(st.st_mode)) { 63 | fprintf(stderr, "hooks dir (./%s) is not a directory. remove it or replace it with a directory.\n", 64 | dir); 65 | return -1; 66 | } 67 | } 68 | 69 | if (access(dir, R_OK) != 0) { 70 | fprintf(stderr, "Can't access hooks dir (./%s): %s\n", 71 | dir, strerror(errno)); 72 | return -1; 73 | } 74 | 75 | return 0; 76 | } 77 | 78 | int clear_hooks(char *dirname) { 79 | DIR *dir; 80 | struct dirent *ent; 81 | char *path = NULL; 82 | char *new_path; 83 | int path_len; 84 | int dirname_len; 85 | int status = 0; 86 | 87 | dirname_len = strlen(dirname); 88 | if ((dir = opendir(dirname)) != NULL) { 89 | while ((ent = readdir(dir)) != NULL) { 90 | if (strcmp(ent->d_name, ".") == 0 || 91 | strcmp(ent->d_name, "..") == 0) { 92 | continue; 93 | } 94 | path_len = dirname_len + strlen(ent->d_name) + 2; 95 | if (path == NULL) { 96 | path = malloc(path_len); 97 | if (path == NULL) { 98 | perror("malloc path failed"); 99 | closedir(dir); 100 | return -1; 101 | } 102 | } else { 103 | new_path = realloc(path, path_len); 104 | if (new_path == NULL) { 105 | perror("realloc path failed"); 106 | closedir(dir); 107 | return -1; 108 | } 109 | path = new_path; 110 | } 111 | snprintf(path, path_len, "%s/%s", dirname, ent->d_name); 112 | if (unlink(path) != 0) { 113 | perror("unlink failed"); 114 | status = -1; 115 | } 116 | } 117 | closedir(dir); 118 | if (path != NULL) { 119 | free(path); 120 | } 121 | } else { 122 | status = -1; 123 | } 124 | 125 | return status; 126 | } 127 | 128 | void *watch_for_file_creation(watch_target *target) { 129 | int length, i; 130 | int fd; 131 | int wd; 132 | int dir_strlen; 133 | char buffer[EVENT_BUF_LEN]; 134 | char *dir = target->dir; 135 | void (*callback)(char *, char *) = target->callback; 136 | int read_content = target->read_content; 137 | free(target); 138 | dir_strlen = strlen(dir); 139 | 140 | // Catch signal sent by stop_watching_hooks() 141 | struct sigaction term_handler = {.sa_handler = sig_handler}; 142 | sigaction(SIGUSR1, &term_handler, NULL); 143 | 144 | fd = inotify_init(); 145 | if (fd < 0) { 146 | perror("inotify_init error"); 147 | exit(EXIT_FAILURE); 148 | } 149 | 150 | struct stat st; 151 | int err = stat(dir, &st); 152 | if (err == -1) { 153 | if (errno == ENOENT) { 154 | fprintf(stderr, "error: %s directory does not exist\n", dir); 155 | } else { 156 | perror("stat error"); 157 | } 158 | exit(EXIT_FAILURE); 159 | } else { 160 | if (!S_ISDIR(st.st_mode)) { 161 | fprintf(stderr, "error: %s is not a directory. remove it or replace it with a directory.\n", dir); 162 | exit(EXIT_FAILURE); 163 | } 164 | } 165 | 166 | if (access(dir, R_OK) != 0) { 167 | perror("error: cannot access hook target directory"); 168 | exit(EXIT_FAILURE); 169 | } 170 | 171 | uint32_t inotify_mask; 172 | if (read_content) { 173 | inotify_mask = IN_CLOSE_WRITE; 174 | } else { 175 | inotify_mask = IN_CREATE; 176 | } 177 | 178 | wd = inotify_add_watch(fd, dir, inotify_mask); 179 | 180 | while (keep_watching) { 181 | length = read(fd, buffer, EVENT_BUF_LEN); 182 | if (length < 0) { 183 | break; 184 | } 185 | 186 | i = 0; 187 | while (i < length) { 188 | struct inotify_event *event = ( struct inotify_event * ) &buffer[ i ]; 189 | if (event->len) { 190 | if (event->mask & inotify_mask) { 191 | if (!(event->mask & IN_ISDIR)) { // file 192 | int path_len = dir_strlen + strlen(event->name) + 2; 193 | char *path = malloc(path_len); 194 | if (path == NULL) { 195 | perror("malloc for file path failed"); 196 | } else { 197 | snprintf(path, path_len, "%s/%s", dir, event->name); 198 | 199 | if (read_content) { 200 | // Read file contents 201 | FILE *fp = fopen(path, "rb"); 202 | char *content = NULL; 203 | if (fp) { 204 | fseek(fp, 0, SEEK_END); 205 | long content_len = ftell(fp); 206 | fseek(fp, 0, SEEK_SET); 207 | content = malloc(content_len + 1); 208 | if (content) { 209 | fread(content, 1, content_len, fp); 210 | content[content_len] = '\0'; 211 | } else { 212 | perror("malloc for file content failed"); 213 | } 214 | fclose(fp); 215 | } else { 216 | perror("fopen failed"); 217 | } 218 | callback(event->name, content); 219 | free(content); 220 | } else { 221 | callback(event->name, NULL); 222 | } 223 | 224 | 225 | // Delete that file 226 | if (unlink(path) != 0) { 227 | perror("unlink failed"); 228 | } 229 | free(path); 230 | } 231 | } 232 | } 233 | } 234 | i += EVENT_SIZE + event->len; 235 | } 236 | } 237 | 238 | inotify_rm_watch(fd, wd); 239 | close(fd); 240 | pthread_exit(0); 241 | } 242 | 243 | void start_watching_hooks(pthread_t *thread, char *dir, void (*callback)(char *, char *), int read_content) { 244 | watch_target *target = malloc(sizeof(watch_target)); 245 | target->dir = dir; 246 | target->callback = callback; 247 | target->read_content = read_content; 248 | pthread_create(thread, NULL, (void * (*)(void *))watch_for_file_creation, target); 249 | watcher_thread = thread; 250 | } 251 | 252 | void stop_watching_hooks() { 253 | keep_watching = 0; 254 | 255 | // When a signal is sent to the pthread, 256 | // blocking read() returns immediately and errno is set to EINTR. 257 | pthread_kill(*watcher_thread, SIGUSR1); 258 | } 259 | -------------------------------------------------------------------------------- /libhook/hook.h: -------------------------------------------------------------------------------- 1 | #ifndef _CLIB_HOOKS_H_ 2 | #define _CLIB_HOOKS_H_ 3 | 4 | #if defined(__cplusplus) 5 | extern "C" { 6 | #endif 7 | 8 | int hooks_create_dir(char *dir); 9 | int clear_hooks(char *dirname); 10 | void start_watching_hooks(pthread_t *thread, char *dir, void (*callback)(char *, char *), int read_content); 11 | void stop_watching_hooks(); 12 | 13 | #if defined(__cplusplus) 14 | } 15 | #endif 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /libpicam/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | #find_package(Boost REQUIRED COMPONENTS program_options) 6 | 7 | #add_library(libpicam picam.cpp) 8 | #find_package(PkgConfig) 9 | #pkg_check_modules(LIBAV REQUIRED libavformat libavcodec) 10 | #include_directories(${LIBAV_INCLUDE_DIRS}) 11 | #pkg_check_modules(LIBFONT REQUIRED harfbuzz fontconfig) 12 | #include_directories(${LIBFONT_INCLUDE_DIRS}) 13 | #target_link_libraries(libpicam picam_option video_encoder timestamp audio log httplivestreaming muxer libstate libhook preview rtsp asound ${LIBFONT_LIBRARIES} lzma pthread ${LIBCAMERA_LINK_LIBRARIES} ${Boost_LIBRARIES}) 14 | #target_link_libraries(libpicam ${LIBAV_LIBRARIES} -static) 15 | #set_target_properties(libpicam PROPERTIES PREFIX "") 16 | 17 | #install(TARGETS libpicam LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 18 | 19 | project(picam) 20 | find_package(PkgConfig) 21 | pkg_check_modules(LIBFONT REQUIRED harfbuzz fontconfig) 22 | include_directories(${LIBFONT_INCLUDE_DIRS}) 23 | 24 | pkg_check_modules(LIBAV REQUIRED libavformat libavcodec) 25 | # Remove "fdk-aac" from LIBAV_LIBRARIES and stores it into LIBAV_WITHOUT_FDK 26 | string(REPLACE fdk-aac "" LIBAV_WITHOUT_FDK "${LIBAV_LIBRARIES}") 27 | include_directories(${LIBAV_INCLUDE_DIRS}) 28 | 29 | add_executable(picam main.cpp picam.cpp) 30 | target_link_libraries(picam picam_option video_encoder timestamp audio log httplivestreaming muxer libstate libhook preview rtsp asound ${LIBFONT_LIBRARIES} ${LIBCAMERA_LINK_LIBRARIES} ${LIBAV_WITHOUT_FDK} lzma pthread) 31 | 32 | # statically link libfdk-aac 33 | target_link_libraries(picam fdk-aac.a) 34 | 35 | set(EXECUTABLES picam) 36 | set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}) 37 | install(TARGETS ${EXECUTABLES} RUNTIME DESTINATION bin) 38 | -------------------------------------------------------------------------------- /libpicam/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "libpicam/picam.hpp" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | Picam* picam; 8 | return picam->getInstance().run(argc, argv); 9 | } 10 | -------------------------------------------------------------------------------- /libpicam/picam.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "core/stream_info.hpp" 14 | #include "core/completed_request.hpp" 15 | #include "video_encoder/video_encoder.hpp" 16 | #include "preview/preview.hpp" 17 | #include "picam_option/picam_option.hpp" 18 | #include "muxer/muxer.hpp" 19 | #include "audio/audio.hpp" 20 | 21 | #define ENABLE_AUTO_GOP_SIZE_CONTROL_FOR_VFR 1 22 | 23 | // enum 24 | #define EXPOSURE_AUTO 0 25 | #define EXPOSURE_NIGHT 1 26 | 27 | typedef std::function EncodeOutputReadyCallback; 28 | 29 | // Pace of PTS 30 | typedef enum { 31 | PTS_SPEED_NORMAL, 32 | PTS_SPEED_UP, 33 | PTS_SPEED_DOWN, 34 | } pts_mode_t; 35 | 36 | class Picam { 37 | public: 38 | // Singleton 39 | static Picam& getInstance() 40 | { 41 | static Picam instance; 42 | return instance; 43 | } 44 | Picam(); 45 | Picam(Picam const&) = delete; 46 | void operator=(Picam const&) = delete; 47 | 48 | ~Picam(); 49 | int run(int argc, char *argv[]); 50 | void print_program_version(); 51 | int parseOptions(int argc, char **argv); 52 | void stop(); 53 | void handleHook(char *filename, char *content); 54 | 55 | // >>> libcamera_app.hpp 56 | enum class MsgType 57 | { 58 | RequestComplete, 59 | Timeout, 60 | Quit 61 | }; 62 | typedef std::variant MsgPayload; 63 | struct Msg 64 | { 65 | Msg(MsgType const &t) : type(t) {} 66 | template 67 | Msg(MsgType const &t, T p) : type(t), payload(std::forward(p)) 68 | { 69 | } 70 | MsgType type; 71 | MsgPayload payload; 72 | }; 73 | 74 | static constexpr unsigned int FLAG_VIDEO_NONE = 0; 75 | static constexpr unsigned int FLAG_VIDEO_RAW = 1; // request raw image stream 76 | static constexpr unsigned int FLAG_VIDEO_JPEG_COLOURSPACE = 2; // force JPEG colour space 77 | 78 | void OpenCamera(); 79 | void CloseCamera(); 80 | 81 | void ConfigureVideo(unsigned int flags = FLAG_VIDEO_NONE); 82 | 83 | void Teardown(); 84 | void StartCamera(); 85 | void StopCamera(); 86 | 87 | Msg Wait(); 88 | void PostMessage(MsgType &t, MsgPayload &p); 89 | 90 | libcamera::Stream *GetStream(std::string const &name, StreamInfo *info = nullptr) const; 91 | libcamera::Stream *VideoStream(StreamInfo *info = nullptr) const; 92 | 93 | std::vector> Mmap(libcamera::FrameBuffer *buffer) const; 94 | 95 | void ShowPreview(CompletedRequestPtr &completed_request, libcamera::Stream *stream); 96 | 97 | StreamInfo GetStreamInfo(libcamera::Stream const *stream) const; 98 | // <<< libcamera_app.hpp 99 | 100 | // >>> libcamera_encoder.hpp 101 | void StartEncoder() 102 | { 103 | createEncoder(); 104 | encoder_->SetInputDoneCallback(std::bind(&Picam::encodeBufferDone, this, std::placeholders::_1)); 105 | encoder_->SetOutputReadyCallback(encode_output_ready_callback_); 106 | } 107 | void SetEncodeOutputReadyCallback(EncodeOutputReadyCallback callback) { encode_output_ready_callback_ = callback; } 108 | void EncodeBuffer(CompletedRequestPtr &completed_request, libcamera::Stream *stream) 109 | { 110 | assert(encoder_); 111 | StreamInfo info = GetStreamInfo(stream); 112 | libcamera::FrameBuffer *buffer = completed_request->buffers[stream]; 113 | libcamera::Span span = Mmap(buffer)[0]; 114 | void *mem = span.data(); 115 | if (!buffer || !mem) 116 | throw std::runtime_error("no buffer to encode"); 117 | auto ts = completed_request->metadata.get(libcamera::controls::SensorTimestamp); 118 | int64_t timestamp_ns = ts ? *ts : buffer->metadata().timestamp; 119 | { 120 | std::lock_guard lock(encode_buffer_queue_mutex_); 121 | encode_buffer_queue_.push(completed_request); // creates a new reference 122 | } 123 | encoder_->EncodeBuffer(buffer->planes()[0].fd.get(), span.size(), mem, info, timestamp_ns / 1000); 124 | } 125 | // VideoOptions *GetOptions() const { return static_cast(options_.get()); } 126 | void StopEncoder() { encoder_.reset(); } 127 | // <<< libcamera_encoder.hpp 128 | 129 | protected: 130 | // >>> libcamera_encoder.hpp 131 | void createEncoder() 132 | { 133 | StreamInfo info; 134 | VideoStream(&info); 135 | if (!info.width || !info.height || !info.stride) { 136 | throw std::runtime_error("video steam is not configured"); 137 | } 138 | encoder_ = std::unique_ptr(new VideoEncoder(this->option, info)); 139 | } 140 | std::unique_ptr encoder_; 141 | // <<< libcamera_encoder.hpp 142 | 143 | private: 144 | Muxer *muxer; 145 | Audio *audio; 146 | HTTPLiveStreaming *hls; 147 | uint8_t *sps_pps = NULL; // Stores H.264 SPS (NAL unit type 7) and PPS (NAL unit type 8) as a single byte array 148 | size_t sps_pps_size; // Size of sps_pps in bytes 149 | int audio_min_value; 150 | int audio_max_value; 151 | PicamOption *option; 152 | int64_t video_current_pts = LLONG_MIN; 153 | int64_t audio_current_pts = 0; 154 | int64_t last_pts = 0; 155 | int64_t time_for_last_pts = 0; // Used in VFR mode 156 | pts_mode_t pts_mode = PTS_SPEED_NORMAL; 157 | 158 | #if ENABLE_AUTO_GOP_SIZE_CONTROL_FOR_VFR 159 | // Variables for variable frame rate 160 | int64_t last_keyframe_pts = 0; 161 | int frames_since_last_keyframe = 0; 162 | #endif 163 | 164 | uint64_t video_frame_count = 0; 165 | uint64_t audio_frame_count = 0; 166 | 167 | // Counter for PTS speed up/down 168 | int speed_up_count = 0; 169 | int speed_down_count = 0; 170 | 171 | int keyframes_count = 0; 172 | struct timespec tsBegin = { 173 | 0, // tv_sec 174 | 0, // tv_nsec 175 | }; 176 | int frame_count_since_keyframe = 0; 177 | 178 | bool is_video_started = false; 179 | bool is_audio_started = false; 180 | int64_t video_start_time; 181 | int64_t audio_start_time; 182 | volatile bool keepRunning = true; 183 | 184 | RecSettings rec_settings; 185 | 186 | void event_loop(); 187 | void setOption(PicamOption *option); 188 | void setHDR(bool enabled); 189 | void setupEncoder(); 190 | void modifyBuffer(CompletedRequestPtr &completed_request); 191 | int64_t get_next_video_pts_vfr(); 192 | int64_t get_next_video_pts_cfr(); 193 | int64_t get_next_video_pts(); 194 | int64_t get_next_audio_pts(); 195 | void videoEncodeDoneCallback(void *mem, size_t size, int64_t timestamp_us, bool keyframe); 196 | void print_audio_timing(); 197 | void check_video_and_audio_started(); 198 | void on_video_and_audio_started(); 199 | void ensure_hls_dir_exists(); 200 | void parse_start_record_file(char *full_filename); 201 | void stopAllThreads(); 202 | void stopAudioThread(); 203 | void stopRecThread(); 204 | void queryCameras(); 205 | int camera_set_exposure_control(char *ex); 206 | int camera_set_ae_metering_mode(char *mode); 207 | int camera_set_exposure_value(); 208 | int camera_set_white_balance(char *wb); 209 | int camera_set_custom_awb_gains(); 210 | 211 | int camera_set_brightness(); 212 | int camera_set_contrast(); 213 | int camera_set_saturation(); 214 | int camera_set_sharpness(); 215 | int camera_set_autofocus_mode(char *mode); 216 | int camera_set_lens_position(); 217 | 218 | // >>> libcamera_app.hpp 219 | template 220 | class MessageQueue 221 | { 222 | public: 223 | template 224 | void Post(U &&msg) 225 | { 226 | std::unique_lock lock(mutex_); 227 | queue_.push(std::forward(msg)); 228 | cond_.notify_one(); 229 | } 230 | T Wait() 231 | { 232 | std::unique_lock lock(mutex_); 233 | cond_.wait(lock, [this] { return !queue_.empty(); }); 234 | T msg = std::move(queue_.front()); 235 | queue_.pop(); 236 | return msg; 237 | } 238 | void Clear() 239 | { 240 | std::unique_lock lock(mutex_); 241 | queue_ = {}; 242 | } 243 | 244 | private: 245 | std::queue queue_; 246 | std::mutex mutex_; 247 | std::condition_variable cond_; 248 | }; 249 | struct PreviewItem 250 | { 251 | PreviewItem() : stream(nullptr) {} 252 | PreviewItem(CompletedRequestPtr &b, libcamera::Stream *s) : completed_request(b), stream(s) {} 253 | PreviewItem &operator=(PreviewItem &&other) 254 | { 255 | completed_request = std::move(other.completed_request); 256 | stream = other.stream; 257 | other.stream = nullptr; 258 | return *this; 259 | } 260 | CompletedRequestPtr completed_request; 261 | libcamera::Stream *stream; 262 | }; 263 | 264 | void setupCapture(); 265 | void makeRequests(); 266 | void queueRequest(CompletedRequest *completed_request); 267 | void requestComplete(libcamera::Request *request); 268 | void previewDoneCallback(int fd); 269 | void startPreview(); 270 | void stopPreview(); 271 | void previewThread(); 272 | void configureDenoise(const std::string &denoise_mode); 273 | 274 | void auto_select_exposure(int width, int height, uint8_t *data, float fps); 275 | void set_exposure_to_night(); 276 | void set_exposure_to_auto(); 277 | float calc_current_real_fps(); 278 | int current_exposure_mode = EXPOSURE_AUTO; 279 | float current_real_fps = -1.0f; 280 | int64_t keyframes_since_exposure_selection = 0; 281 | 282 | std::unique_ptr camera_manager_; 283 | std::shared_ptr camera_; 284 | bool camera_acquired_ = false; 285 | std::unique_ptr configuration_; 286 | std::map>> mapped_buffers_; 287 | std::map streams_; 288 | libcamera::FrameBufferAllocator *allocator_ = nullptr; 289 | std::map> frame_buffers_; 290 | std::vector> requests_; 291 | std::mutex completed_requests_mutex_; 292 | std::set completed_requests_; 293 | bool camera_started_ = false; 294 | std::mutex camera_stop_mutex_; 295 | MessageQueue msg_queue_; 296 | // Related to the preview window. 297 | std::unique_ptr preview_; 298 | std::map preview_completed_requests_; 299 | std::mutex preview_mutex_; 300 | std::mutex preview_item_mutex_; 301 | PreviewItem preview_item_; 302 | std::condition_variable preview_cond_var_; 303 | bool preview_abort_ = false; 304 | uint32_t preview_frames_displayed_ = 0; 305 | uint32_t preview_frames_dropped_ = 0; 306 | std::thread preview_thread_; 307 | // For setting camera controls. 308 | std::mutex control_mutex_; 309 | libcamera::ControlList controls_; 310 | // Other: 311 | // uint64_t last_timestamp_; 312 | // uint64_t sequence_ = 0; 313 | // PostProcessor post_processor_; 314 | // <<< libcamera_app.hpp 315 | 316 | // >>> libcamera_encoder.hpp 317 | void encodeBufferDone(void *mem) 318 | { 319 | // mem is nullptr 320 | // std::cout << "encodeBufferDone: mem=" << mem << std::endl; 321 | 322 | // If non-NULL, mem would indicate which buffer has been completed, but 323 | // currently we're just assuming everything is done in order. (We could 324 | // handle this by replacing the queue with a vector of 325 | // pairs.) 326 | assert(mem == nullptr); 327 | { 328 | std::lock_guard lock(encode_buffer_queue_mutex_); 329 | if (encode_buffer_queue_.empty()) 330 | throw std::runtime_error("no buffer available to return"); 331 | encode_buffer_queue_.pop(); // drop shared_ptr reference 332 | } 333 | } 334 | 335 | std::queue encode_buffer_queue_; 336 | std::mutex encode_buffer_queue_mutex_; 337 | EncodeOutputReadyCallback encode_output_ready_callback_; 338 | // <<< libcamera_encoder.hpp 339 | }; 340 | -------------------------------------------------------------------------------- /libstate/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(libstate STATIC state.c) 6 | target_link_libraries(libstate) 7 | set_target_properties(libstate PROPERTIES PREFIX "") 8 | 9 | install(TARGETS libstate LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 10 | -------------------------------------------------------------------------------- /libstate/state.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "state.h" 9 | 10 | char *default_dir; 11 | 12 | // Create state dir if it does not exist 13 | int state_create_dir(char *dir) { 14 | struct stat st; 15 | int err; 16 | 17 | err = stat(dir, &st); 18 | if (err == -1) { 19 | if (errno == ENOENT) { 20 | // Check if this is a broken symbolic link 21 | err = lstat(dir, &st); 22 | if (err == 0 && S_ISLNK(st.st_mode)) { 23 | fprintf(stderr, "error: ./%s is a broken symbolic link\n", dir); 24 | return -1; 25 | } 26 | 27 | // create directory 28 | if (mkdir(dir, 0755) == 0) { // success 29 | fprintf(stderr, "created state dir: ./%s\n", dir); 30 | } else { // error 31 | fprintf(stderr, "error creating state dir (./%s): %s\n", 32 | dir, strerror(errno)); 33 | return -1; 34 | } 35 | } else { 36 | perror("stat state dir"); 37 | return -1; 38 | } 39 | } else { 40 | if (!S_ISDIR(st.st_mode)) { 41 | fprintf(stderr, "error: state dir (./%s) is not a directory. remove it or replace it with a directory.\n", 42 | dir); 43 | return -1; 44 | } 45 | } 46 | 47 | if (access(dir, R_OK) != 0) { 48 | fprintf(stderr, "Can't access state dir (./%s): %s\n", 49 | dir, strerror(errno)); 50 | return -1; 51 | } 52 | 53 | return 0; 54 | } 55 | 56 | void state_default_dir(const char *dir) { 57 | default_dir = (char *)dir; 58 | } 59 | 60 | void state_set(const char *dir, const char *name, const char *value) { 61 | FILE *fp; 62 | char *path; 63 | int path_len; 64 | struct stat st; 65 | int err; 66 | 67 | if (dir == NULL) { 68 | dir = default_dir; 69 | } 70 | 71 | err = stat(dir, &st); 72 | if (err == -1) { 73 | if (errno == ENOENT) { 74 | fprintf(stderr, "Error: %s directory does not exist\n", dir); 75 | } else { 76 | perror("stat error"); 77 | } 78 | exit(EXIT_FAILURE); 79 | } else { 80 | if (!S_ISDIR(st.st_mode)) { 81 | fprintf(stderr, "Error: %s is not a directory\n", dir); 82 | exit(EXIT_FAILURE); 83 | } 84 | } 85 | 86 | path_len = strlen(dir) + strlen(name) + 2; 87 | path = malloc(path_len); 88 | if (path == NULL) { 89 | perror("malloc path"); 90 | return; 91 | } 92 | snprintf(path, path_len, "%s/%s", dir, name); 93 | fp = fopen(path, "w"); 94 | if (fp == NULL) { 95 | perror("State file open failed"); 96 | return; 97 | } 98 | fwrite(value, 1, strlen(value), fp); 99 | fclose(fp); 100 | free(path); 101 | } 102 | 103 | void state_get(const char *dir, const char *name, char **buf) { 104 | FILE *fp; 105 | char *path; 106 | int path_len; 107 | int size; 108 | struct stat st; 109 | int err; 110 | 111 | if (dir == NULL) { 112 | dir = default_dir; 113 | } 114 | 115 | err = stat(dir, &st); 116 | if (err == -1) { 117 | if (errno == ENOENT) { 118 | fprintf(stderr, "Error: %s directory does not exist\n", dir); 119 | } else { 120 | perror("stat error"); 121 | } 122 | exit(EXIT_FAILURE); 123 | } else { 124 | if (!S_ISDIR(st.st_mode)) { 125 | fprintf(stderr, "Error: %s is not a directory\n", dir); 126 | exit(EXIT_FAILURE); 127 | } 128 | } 129 | 130 | path_len = strlen(dir) + strlen(name) + 2; 131 | path = malloc(path_len); 132 | if (path == NULL) { 133 | perror("malloc path"); 134 | return; 135 | } 136 | snprintf(path, path_len, "%s/%s", dir, name); 137 | fp = fopen(path, "r"); 138 | if (fp == NULL) { 139 | perror("State file open failed"); 140 | return; 141 | } 142 | fseek(fp, 0, SEEK_END); 143 | size = ftell(fp); 144 | fseek(fp, 0, SEEK_SET); 145 | *buf = malloc(size); 146 | if (*buf == NULL) { 147 | perror("Can't malloc for buffer"); 148 | return; 149 | } 150 | fread(*buf, 1, size, fp); 151 | fclose(fp); 152 | free(path); 153 | } 154 | -------------------------------------------------------------------------------- /libstate/state.h: -------------------------------------------------------------------------------- 1 | #ifndef _CLIB_STATE_H_ 2 | #define _CLIB_STATE_H_ 3 | 4 | #if defined(__cplusplus) 5 | extern "C" { 6 | #endif 7 | 8 | int state_create_dir(char *dir); 9 | void state_default_dir(const char *dir); 10 | void state_set(const char *dir, const char *name, const char *value); 11 | void state_get(const char *dir, const char *name, char **buf); 12 | 13 | #if defined(__cplusplus) 14 | } 15 | #endif 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /log/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(log STATIC log.c) 6 | target_link_libraries(log) 7 | 8 | install(TARGETS log LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /log/log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "log.h" 5 | 6 | static int log_level = LOG_LEVEL_DEBUG; 7 | static FILE *out_stream = NULL; 8 | 9 | void log_set_level(int level) { 10 | log_level = level; 11 | } 12 | 13 | int log_get_level() { 14 | return log_level; 15 | } 16 | 17 | void log_set_stream(FILE *stream) { 18 | out_stream = stream; 19 | } 20 | 21 | void log_hex(int msg_log_level, uint8_t *data, int len) { 22 | int i; 23 | 24 | if (msg_log_level < log_level) { 25 | return; 26 | } 27 | 28 | for (i = 0; i < len; i++) { 29 | log_msg_level(msg_log_level, "%02x", data[i]); 30 | } 31 | } 32 | 33 | void log_msg(int msg_log_level, const char *format, const va_list args) { 34 | if (out_stream == NULL) { 35 | out_stream = stdout; 36 | } 37 | 38 | if (msg_log_level >= log_level) { 39 | vfprintf(out_stream, format, args); 40 | } 41 | } 42 | 43 | void log_msg_level(int msg_log_level, const char *format, ...) { 44 | va_list args; 45 | va_start(args, format); 46 | log_msg(msg_log_level, format, args); 47 | va_end(args); 48 | } 49 | 50 | void log_debug(const char *format, ...) { 51 | va_list args; 52 | va_start(args, format); 53 | log_msg(LOG_LEVEL_DEBUG, format, args); 54 | va_end(args); 55 | } 56 | 57 | void log_info(const char *format, ...) { 58 | va_list args; 59 | va_start(args, format); 60 | log_msg(LOG_LEVEL_INFO, format, args); 61 | va_end(args); 62 | } 63 | 64 | void log_warn(const char *format, ...) { 65 | va_list args; 66 | va_start(args, format); 67 | log_msg(LOG_LEVEL_WARN, format, args); 68 | va_end(args); 69 | } 70 | 71 | void log_error(const char *format, ...) { 72 | va_list args; 73 | va_start(args, format); 74 | log_msg(LOG_LEVEL_ERROR, format, args); 75 | va_end(args); 76 | } 77 | 78 | void log_fatal(const char *format, ...) { 79 | va_list args; 80 | va_start(args, format); 81 | log_msg(LOG_LEVEL_FATAL, format, args); 82 | va_end(args); 83 | } 84 | -------------------------------------------------------------------------------- /log/log.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef _LOG_H_ 6 | #define _LOG_H_ 7 | 8 | #include 9 | #include 10 | 11 | enum { 12 | LOG_LEVEL_DEBUG, 13 | LOG_LEVEL_INFO, 14 | LOG_LEVEL_WARN, 15 | LOG_LEVEL_ERROR, 16 | LOG_LEVEL_FATAL, 17 | LOG_LEVEL_OFF, 18 | }; 19 | 20 | void log_set_level(int level); 21 | int log_get_level(); 22 | void log_set_stream(FILE *stream); 23 | void log_hex(int msg_log_level, uint8_t *data, int len); 24 | void log_msg(int level, const char *format, const va_list args); 25 | void log_msg_level(int msg_log_level, const char *format, ...); 26 | void log_debug(const char *format, ...); 27 | void log_info(const char *format, ...); 28 | void log_warn(const char *format, ...); 29 | void log_error(const char *format, ...); 30 | void log_fatal(const char *format, ...); 31 | 32 | #endif 33 | 34 | #ifdef __cplusplus 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /mpegts/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(mpegts STATIC mpegts.c) 6 | target_link_libraries(mpegts avutil avcodec) 7 | 8 | install(TARGETS mpegts LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /mpegts/mpegts.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "mpegts.h" 5 | 6 | static char errbuf[1024]; 7 | 8 | AVCodecContext *setup_video_stream(AVFormatContext *format_ctx, MpegTSCodecSettings *settings) { 9 | AVStream *video_stream; 10 | AVCodecContext *video_codec_ctx; 11 | 12 | video_stream = avformat_new_stream(format_ctx, 0); 13 | if (!video_stream) { 14 | fprintf(stderr, "avformat_new_stream failed\n"); 15 | exit(EXIT_FAILURE); 16 | } 17 | video_stream->id = format_ctx->nb_streams - 1; 18 | 19 | const AVCodec *codec = avcodec_find_decoder(video_stream->codecpar->codec_id); 20 | video_codec_ctx = avcodec_alloc_context3(codec); 21 | 22 | video_codec_ctx->codec_id = AV_CODEC_ID_H264; 23 | video_codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; 24 | video_codec_ctx->codec_tag = 0; 25 | video_codec_ctx->bit_rate = settings->video_bitrate; 26 | 27 | video_codec_ctx->profile = settings->video_profile; 28 | video_codec_ctx->level = settings->video_level; 29 | 30 | video_stream->time_base.num = 1; 31 | video_stream->time_base.den = 180000; 32 | video_codec_ctx->time_base.num = 1; 33 | video_codec_ctx->time_base.den = 180000; 34 | video_codec_ctx->framerate = av_mul_q(video_codec_ctx->time_base, (AVRational){2,1}); 35 | video_codec_ctx->pix_fmt = 0; 36 | video_codec_ctx->width = settings->video_width; 37 | video_codec_ctx->height = settings->video_height; 38 | video_codec_ctx->has_b_frames = 0; 39 | video_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; 40 | 41 | avcodec_parameters_from_context(video_stream->codecpar, video_codec_ctx); 42 | return video_codec_ctx; 43 | } 44 | 45 | static int is_sample_fmt_supported(const AVCodecContext *codec_ctx) { 46 | const enum AVSampleFormat *sample_formats = NULL; 47 | int num_sample_formats; 48 | int ret; 49 | 50 | ret = avcodec_get_supported_config(codec_ctx, NULL, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, 51 | (const void**)&sample_formats, &num_sample_formats); 52 | if (ret < 0) { 53 | fprintf(stderr, "avcodec_get_supported_config failed: %s\n", av_err2str(ret)); 54 | exit(EXIT_FAILURE); 55 | } 56 | for (int i = 0; i < num_sample_formats; i++) { 57 | if (sample_formats[i] == codec_ctx->sample_fmt) { 58 | return 1; 59 | } 60 | } 61 | return 0; 62 | } 63 | 64 | AVCodecContext *setup_audio_stream(AVFormatContext *format_ctx, MpegTSCodecSettings *settings) { 65 | const AVCodec *aac_codec; 66 | AVCodecContext *audio_codec_ctx = NULL; 67 | AVStream *audio_stream; 68 | int ret; 69 | 70 | aac_codec = avcodec_find_encoder_by_name("libfdk_aac"); 71 | if (!aac_codec) { 72 | fprintf(stderr, "codec libfdk_aac is not available. Install ffmpeg with libfdk_aac support.\n"); 73 | exit(EXIT_FAILURE); 74 | } 75 | 76 | audio_stream = avformat_new_stream(format_ctx, aac_codec); 77 | if (!audio_stream) { 78 | fprintf(stderr, "avformat_new_stream for audio error\n"); 79 | exit(EXIT_FAILURE); 80 | } 81 | audio_stream->id = format_ctx->nb_streams - 1; 82 | audio_codec_ctx = avcodec_alloc_context3(aac_codec); 83 | 84 | audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; 85 | if ( ! is_sample_fmt_supported(audio_codec_ctx) ) { 86 | fprintf(stderr, "Sample format %s is not supported\n", 87 | av_get_sample_fmt_name(audio_codec_ctx->sample_fmt)); 88 | exit(EXIT_FAILURE); 89 | } 90 | 91 | audio_stream->time_base.num = 1; 92 | audio_stream->time_base.den = settings->audio_sample_rate; 93 | audio_codec_ctx->time_base.num = 1; 94 | audio_codec_ctx->time_base.den = settings->audio_sample_rate; 95 | audio_codec_ctx->framerate = audio_codec_ctx->time_base; 96 | audio_codec_ctx->bit_rate = settings->audio_bit_rate; 97 | audio_codec_ctx->codec_type = AVMEDIA_TYPE_AUDIO; 98 | audio_codec_ctx->profile = settings->audio_profile; 99 | audio_codec_ctx->sample_rate = settings->audio_sample_rate; 100 | if (settings->audio_channels == 2) { 101 | AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_STEREO; 102 | audio_codec_ctx->ch_layout = ch_layout; 103 | } else { 104 | AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_MONO; 105 | audio_codec_ctx->ch_layout = ch_layout; 106 | } 107 | 108 | ret = avcodec_open2(audio_codec_ctx, aac_codec, NULL); 109 | if (ret < 0) { 110 | av_strerror(ret, errbuf, sizeof(errbuf)); 111 | fprintf(stderr, "avcodec_open2 failed: %s\n", errbuf); 112 | exit(EXIT_FAILURE); 113 | } 114 | 115 | // This must be called after avcodec_open2() 116 | avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_ctx); 117 | 118 | return audio_codec_ctx; 119 | } 120 | 121 | void mpegts_destroy_context(AVFormatContext *format_ctx) { 122 | // unsigned int i; 123 | // for (i = 0; i < format_ctx->nb_streams; i++) { 124 | // avcodec_close(format_ctx->streams[i]->codec); 125 | // } 126 | avformat_free_context(format_ctx); 127 | } 128 | 129 | void mpegts_close_stream(AVFormatContext *format_ctx) { 130 | av_write_trailer(format_ctx); 131 | avio_close(format_ctx->pb); 132 | } 133 | 134 | void mpegts_close_stream_without_trailer(AVFormatContext *format_ctx) { 135 | avio_close(format_ctx->pb); 136 | } 137 | 138 | void mpegts_open_stream(AVFormatContext *format_ctx, char *outputfilename, int dump_format) { 139 | int ret; 140 | 141 | if (dump_format) { 142 | av_dump_format(format_ctx, 0, outputfilename, 1); 143 | } 144 | 145 | if (strcmp(outputfilename, "-") == 0) { 146 | outputfilename = "pipe:1"; 147 | } 148 | 149 | ret = avio_open(&format_ctx->pb, outputfilename, AVIO_FLAG_WRITE); 150 | if (ret < 0) { 151 | av_strerror(ret, errbuf, sizeof(errbuf)); 152 | fprintf(stderr, "avio_open for %s failed: %s\n", outputfilename, errbuf); 153 | exit(EXIT_FAILURE); 154 | } 155 | 156 | if (avformat_write_header(format_ctx, NULL)) { 157 | fprintf(stderr, "avformat_write_header failed\n"); 158 | exit(EXIT_FAILURE); 159 | } 160 | } 161 | 162 | void mpegts_open_stream_without_header(AVFormatContext *format_ctx, char *outputfilename, int dump_format) { 163 | int ret; 164 | 165 | if (dump_format) { 166 | av_dump_format(format_ctx, 0, outputfilename, 1); 167 | } 168 | 169 | if (strcmp(outputfilename, "-") == 0) { 170 | outputfilename = "pipe:1"; 171 | } 172 | 173 | ret = avio_open(&format_ctx->pb, outputfilename, AVIO_FLAG_WRITE); 174 | if (ret < 0) { 175 | av_strerror(ret, errbuf, sizeof(errbuf)); 176 | fprintf(stderr, "avio_open for %s failed: %s\n", outputfilename, errbuf); 177 | exit(EXIT_FAILURE); 178 | } 179 | } 180 | 181 | MpegTSContext _mpegts_create_context(int use_video, int use_audio, MpegTSCodecSettings *settings) { 182 | AVFormatContext *format_ctx; 183 | AVCodecContext *codec_context_video = NULL; 184 | AVCodecContext *codec_context_audio = NULL; 185 | 186 | format_ctx = avformat_alloc_context(); 187 | if (!format_ctx) { 188 | fprintf(stderr, "avformat_alloc_context failed\n"); 189 | exit(EXIT_FAILURE); 190 | } 191 | format_ctx->oformat = av_guess_format("mpegts", NULL, NULL); 192 | if (!format_ctx->oformat) { 193 | fprintf(stderr, "av_guess_format failed\n"); 194 | exit(EXIT_FAILURE); 195 | } 196 | 197 | #if !(AUDIO_ONLY) 198 | if (use_video) { 199 | codec_context_video = setup_video_stream(format_ctx, settings); 200 | } 201 | #endif 202 | if (use_audio) { 203 | codec_context_audio = setup_audio_stream(format_ctx, settings); 204 | } 205 | 206 | return (MpegTSContext){ 207 | format_ctx, 208 | codec_context_video, 209 | codec_context_audio, 210 | }; 211 | } 212 | 213 | MpegTSContext mpegts_create_context(MpegTSCodecSettings *settings) { 214 | return _mpegts_create_context(1, 1, settings); 215 | } 216 | 217 | MpegTSContext mpegts_create_context_video_only(MpegTSCodecSettings *settings) { 218 | return _mpegts_create_context(1, 0, settings); 219 | } 220 | 221 | MpegTSContext mpegts_create_context_audio_only(MpegTSCodecSettings *settings) { 222 | return _mpegts_create_context(0, 1, settings); 223 | } 224 | -------------------------------------------------------------------------------- /mpegts/mpegts.h: -------------------------------------------------------------------------------- 1 | #ifndef _CLIB_MPEGTS_H_ 2 | #define _CLIB_MPEGTS_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | 11 | typedef struct MpegTSCodecSettings { 12 | int audio_sample_rate; // e.g. 22050 13 | int audio_bit_rate; // e.g. 24000 14 | int audio_channels; // e.g. 1 15 | int audio_profile; // e.g. AV_PROFILE_AAC_LOW 16 | int video_profile; // e.g. AV_PROFILE_H264_MAIN 17 | int video_level; // e.g. 41 18 | long video_bitrate; 19 | int video_width; 20 | int video_height; 21 | } MpegTSCodecSettings; 22 | 23 | typedef struct MpegTSContext { 24 | AVFormatContext *format_context; 25 | AVCodecContext *codec_context_video; 26 | AVCodecContext *codec_context_audio; 27 | } MpegTSContext; 28 | 29 | MpegTSContext mpegts_create_context(MpegTSCodecSettings *settings); 30 | MpegTSContext mpegts_create_context_video_only(MpegTSCodecSettings *settings); 31 | MpegTSContext mpegts_create_context_audio_only(MpegTSCodecSettings *settings); 32 | void mpegts_set_config(long bitrate, int width, int height); 33 | void mpegts_open_stream(AVFormatContext *format_ctx, char *filename, int dump_format); 34 | void mpegts_open_stream_without_header(AVFormatContext *format_ctx, char *filename, int dump_format); 35 | void mpegts_close_stream(AVFormatContext *format_ctx); 36 | void mpegts_close_stream_without_trailer(AVFormatContext *format_ctx); 37 | void mpegts_destroy_context(AVFormatContext *format_ctx); 38 | 39 | #ifdef __cplusplus 40 | } 41 | #endif 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /muxer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(muxer STATIC muxer.cpp) 6 | target_link_libraries(muxer) 7 | 8 | install(TARGETS muxer LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | -------------------------------------------------------------------------------- /muxer/muxer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "httplivestreaming/httplivestreaming.h" 6 | #include "picam_option/picam_option.hpp" 7 | 8 | #define sizeof_member(type, member) sizeof(((type *)0)->member) 9 | 10 | typedef struct EncodedPacket { 11 | int64_t pts; // AVPacket.pts (presentation timestamp) 12 | uint8_t *data; // AVPacket.data (payload) 13 | int size; // AVPacket.size (size of payload in bytes) 14 | int stream_index; // AVPacket.stream_index 15 | int flags; // AVPacket.flags (keyframe, etc.) 16 | } EncodedPacket; 17 | 18 | // Recording settings 19 | typedef struct RecSettings { 20 | char recording_dest_dir[1024]; 21 | char recording_basename[256]; 22 | // Directory to put recorded MPEG-TS files 23 | char rec_dir[256]; 24 | char rec_tmp_dir[256]; 25 | char rec_archive_dir[1024]; 26 | } RecSettings; 27 | 28 | class Muxer 29 | { 30 | public: 31 | Muxer(PicamOption *option); 32 | ~Muxer(); 33 | void setup(MpegTSCodecSettings *codec_settings, HTTPLiveStreaming *hls); 34 | int write_encoded_packets(int max_packets, int origin_pts); 35 | void start_record(RecSettings *rec_settings); 36 | void *rec_start(); 37 | void write_frame(AVPacket *pkt); 38 | void add_encoded_packet(int64_t pts, uint8_t *data, int size, int stream_index, int flags); 39 | void prepare_encoded_packets(float video_fps, float audio_fps); 40 | void stop_record(); 41 | void onFrameArrive(EncodedPacket *encoded_packet); 42 | void prepareForDestroy(); 43 | void mark_keyframe_packet(); 44 | int set_record_buffer_keyframes(int newsize); 45 | void setup_tcp_output(); 46 | void teardown_tcp_output(); 47 | 48 | // how many keyframes should we look back for the next recording 49 | int recording_look_back_keyframes; 50 | 51 | int record_buffer_keyframes = 5; 52 | 53 | private: 54 | PicamOption *option; 55 | pthread_t rec_thread; 56 | int is_disk_almost_full(); 57 | void free_encoded_packets(); 58 | void check_record_duration(); 59 | void *rec_thread_stop(int skip_cleanup); 60 | void flush_record(); 61 | HTTPLiveStreaming *hls; 62 | RecSettings *rec_settings; 63 | EncodedPacket **encoded_packets; // circular buffer that stores encoded audio and video 64 | int encoded_packets_size; // the number of EncodedPacket that can be stored in encoded_packets 65 | int current_encoded_packet = -1; // write pointer of encoded_packets array that holds latest encoded audio or video 66 | int *keyframe_pointers = NULL; // circular buffer that stores where keyframe occurs within encoded_packets 67 | int current_keyframe_pointer = -1; // write pointer of keyframe_pointers array 68 | int is_keyframe_pointers_filled = 0; // will be changed to 1 once encoded_packets is fully filled 69 | int rec_thread_frame = 0; 70 | AVFormatContext *rec_format_ctx; 71 | time_t rec_start_time; 72 | 73 | char recording_basename[sizeof_member(RecSettings, recording_basename)] = {}; 74 | char recording_filepath[ 75 | sizeof_member(RecSettings, rec_dir) - 1 + 76 | sizeof(recording_basename) - 1 + 77 | 10 + // %d 78 | 5 + // "/-.ts" 79 | 1 // '\0' 80 | ] = {}; // 526 81 | char recording_tmp_filepath[ 82 | sizeof_member(RecSettings, rec_tmp_dir) - 1 + 83 | sizeof(recording_basename) - 1 + 84 | 1 + // "/" 85 | 1 // '\0' 86 | ] = {}; // 512 87 | char recording_archive_filepath[ 88 | sizeof_member(RecSettings, recording_dest_dir) - 1 + 89 | sizeof(recording_basename) - 1 + 90 | 1 + // "/" 91 | 1 // '\0' 92 | ] = {}; // 1280 93 | int is_recording = 0; 94 | 95 | MpegTSCodecSettings *codec_settings; 96 | MpegTSContext mpegts_ctx; 97 | 98 | int rec_thread_needs_exit = 0; 99 | int rec_thread_needs_flush = 0; 100 | int rec_thread_needs_write = 0; 101 | int flush_recording_seconds = 5; // Flush recording data every 5 seconds 102 | 103 | int video_send_keyframe_count = 0; 104 | int64_t video_frame_count = 0; 105 | 106 | // tcp output 107 | AVFormatContext *tcp_ctx; 108 | pthread_mutex_t tcp_mutex = PTHREAD_MUTEX_INITIALIZER; 109 | 110 | // hls output 111 | pthread_mutex_t mutex_writing = PTHREAD_MUTEX_INITIALIZER; 112 | }; 113 | 114 | extern "C" { 115 | void encoded_packet_to_avpacket(EncodedPacket *encoded_packet, AVPacket *av_packet); 116 | } 117 | -------------------------------------------------------------------------------- /picam_option/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(picam_option STATIC picam_option.cpp) 6 | target_link_libraries(picam_option) 7 | 8 | install(TARGETS picam_option LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /picam_option/picam_option.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | extern "C" { 7 | #include 8 | } 9 | 10 | #include "log/log.h" 11 | #include "text/text.h" 12 | 13 | #define PROGRAM_NAME "picam" 14 | #define PROGRAM_VERSION "2.0.12" 15 | 16 | typedef struct white_balance_option { 17 | const char *name; 18 | int32_t control; 19 | } white_balance_option; 20 | const white_balance_option white_balance_options[] = { 21 | { "off", libcamera::controls::AwbCustom }, 22 | { "custom", libcamera::controls::AwbCustom }, 23 | { "auto", libcamera::controls::AwbAuto }, 24 | { "cloudy", libcamera::controls::AwbCloudy }, 25 | { "tungsten", libcamera::controls::AwbTungsten }, 26 | { "fluorescent", libcamera::controls::AwbFluorescent }, 27 | { "incandescent", libcamera::controls::AwbIncandescent }, 28 | { "indoor", libcamera::controls::AwbIndoor }, 29 | { "daylight", libcamera::controls::AwbDaylight }, 30 | }; 31 | 32 | typedef struct exposure_control_option { 33 | const char *name; 34 | int32_t control; 35 | } exposure_control_option; 36 | const exposure_control_option exposure_control_options[] = { 37 | { "custom", libcamera::controls::ExposureCustom }, 38 | { "normal", libcamera::controls::ExposureNormal }, 39 | { "short", libcamera::controls::ExposureShort }, 40 | { "long", libcamera::controls::ExposureLong }, 41 | }; 42 | 43 | typedef struct exposure_metering_option { 44 | const char *name; 45 | int32_t metering; 46 | } exposure_metering_option; 47 | const exposure_metering_option exposure_metering_options[] = { 48 | { "center", libcamera::controls::MeteringCentreWeighted }, 49 | { "spot", libcamera::controls::MeteringSpot }, 50 | { "matrix", libcamera::controls::MeteringMatrix }, 51 | { "custom", libcamera::controls::MeteringCustom }, 52 | }; 53 | 54 | typedef struct video_avc_profile_option { 55 | const char *name; 56 | v4l2_mpeg_video_h264_profile profile; 57 | int ff_profile; // AVCodecContext.profile 58 | } video_avc_profile_option; 59 | const video_avc_profile_option video_avc_profile_options[] = { 60 | { "constrained_baseline", V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE, AV_PROFILE_H264_CONSTRAINED_BASELINE }, 61 | { "baseline", V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE, AV_PROFILE_H264_BASELINE }, 62 | { "main", V4L2_MPEG_VIDEO_H264_PROFILE_MAIN, AV_PROFILE_H264_MAIN }, 63 | { "high", V4L2_MPEG_VIDEO_H264_PROFILE_HIGH, AV_PROFILE_H264_HIGH }, 64 | }; 65 | 66 | typedef struct video_avc_level_option { 67 | const char *name; 68 | v4l2_mpeg_video_h264_level level; 69 | int ff_level; // AVCodecContext.level 70 | } video_avc_level_option; 71 | const video_avc_level_option video_avc_level_options[] = { 72 | // Level < 3.0 is not supported by the encoder 73 | { "3", V4L2_MPEG_VIDEO_H264_LEVEL_3_0, 30 }, 74 | { "3.0", V4L2_MPEG_VIDEO_H264_LEVEL_3_0, 30 }, 75 | { "3.1", V4L2_MPEG_VIDEO_H264_LEVEL_3_1, 31 }, 76 | { "3.2", V4L2_MPEG_VIDEO_H264_LEVEL_3_2, 32 }, 77 | { "4", V4L2_MPEG_VIDEO_H264_LEVEL_4_0, 40 }, 78 | { "4.0", V4L2_MPEG_VIDEO_H264_LEVEL_4_0, 40 }, 79 | { "4.1", V4L2_MPEG_VIDEO_H264_LEVEL_4_1, 41 }, 80 | { "4.2", V4L2_MPEG_VIDEO_H264_LEVEL_4_2, 42 }, 81 | // Level >= 5.0 is not supported by the encoder 82 | }; 83 | 84 | typedef struct video_autofocus_mode_option { 85 | const char *name; 86 | int32_t af_mode; 87 | } video_autofocus_mode_option; 88 | const video_autofocus_mode_option video_autofocus_mode_options[] = { 89 | { "manual", libcamera::controls::AfModeManual }, 90 | { "continuous", libcamera::controls::AfModeContinuous }, 91 | }; 92 | 93 | class PicamOption 94 | { 95 | public: 96 | PicamOption(); 97 | ~PicamOption(); 98 | void print_program_version(); 99 | void print_usage(); 100 | int parse(int argc, char **argv); 101 | 102 | // Directory to put recorded MPEG-TS files 103 | char *rec_dir = (char *)"rec"; 104 | char *rec_tmp_dir = (char *)"rec/tmp"; 105 | char *rec_archive_dir = (char *)"rec/archive"; 106 | 107 | // If true, query camera capabilities and exit 108 | int query_and_exit = 0; 109 | 110 | // If this value is 1, audio capturing is disabled. 111 | int disable_audio_capturing = 0; 112 | 113 | int log_level = LOG_LEVEL_INFO; 114 | int sensor_mode = -1; 115 | int video_width = 1920; 116 | int video_height = 1080; 117 | float video_fps = 30.0f; 118 | float min_fps = -1.0f; 119 | float max_fps = -1.0f; 120 | int video_pts_step = 0; 121 | int audio_pts_step = 0; 122 | int is_video_pts_step_specified = 0; 123 | int video_gop_size = 0; 124 | int is_video_gop_size_specified = 0; 125 | // int video_rotation = 0; 126 | int video_hflip = 0; 127 | int video_vflip = 0; 128 | long video_bitrate = 3000 * 1000; // 3 Mbps 129 | char video_avc_profile[21] = "high"; 130 | char video_avc_level[4] = "4.1"; 131 | int video_qp_min = -1; 132 | int video_qp_max = -1; 133 | int video_qp_initial = -1; 134 | int video_slice_dquant = -1; 135 | char alsa_dev[256] = "hw:0,0"; 136 | int is_audio_preview_enabled = 0; 137 | char audio_preview_dev[256] = "plughw:0,0"; 138 | long audio_bitrate = 40000; // 40 Kbps 139 | int is_audio_channels_specified = 0; 140 | int audio_channels = 1; 141 | int audio_preview_channels; // will be calculated later 142 | int audio_sample_rate = 48000; 143 | int audio_period_size; // will be calculated later 144 | int is_hlsout_enabled = 0; 145 | char hls_output_dir[256] = "/run/shm/video"; 146 | int hls_keyframes_per_segment = 1; 147 | int hls_number_of_segments = 3; 148 | int is_rtspout_enabled = 0; 149 | char rtsp_video_control_path[256] = "/tmp/node_rtsp_rtmp_videoControl"; 150 | char rtsp_audio_control_path[256] = "/tmp/node_rtsp_rtmp_audioControl"; 151 | char rtsp_video_data_path[256] = "/tmp/node_rtsp_rtmp_videoData"; 152 | char rtsp_audio_data_path[256] = "/tmp/node_rtsp_rtmp_audioData"; 153 | int is_tcpout_enabled = 0; 154 | char tcp_output_dest[256]; 155 | int is_auto_exposure_enabled = 0; 156 | int is_vfr_enabled = 0; 157 | unsigned int camera_id = 0; 158 | float auto_exposure_threshold = 5.0f; 159 | 160 | float roi_left = 0.0f; 161 | float roi_top = 0.0f; 162 | float roi_width = 1.0f; 163 | float roi_height = 1.0f; 164 | 165 | char white_balance[13] = "auto"; 166 | char exposure_control[14] = "auto"; 167 | 168 | // Red gain used when AWB is off 169 | float awb_red_gain = 0.0f; 170 | 171 | // Blue gain used when AWB is off 172 | float awb_blue_gain = 0.0f; 173 | 174 | char exposure_metering[8] = "average"; 175 | 176 | int manual_exposure_compensation = 0; // EV compensation 177 | float exposure_compensation = 0.0f; 178 | int manual_exposure_aperture = 0; // f-number 179 | float exposure_aperture = 0.0f; 180 | int manual_exposure_shutter_speed = 0; // in microseconds 181 | unsigned int exposure_shutter_speed = 0; 182 | int manual_exposure_sensitivity = 0; // ISO 183 | unsigned int exposure_sensitivity = 0; 184 | 185 | // https://libcamera.org/api-html/namespacelibcamera_1_1controls.html 186 | // Positive values (up to 1.0) produce brighter images; 187 | // negative values (up to -1.0) produce darker images and 0.0 leaves pixels unchanged. 188 | float video_brightness = 0.0f; 189 | 190 | // 1.0 = Normal contrast; larger values produce images with more contrast 191 | float video_contrast = 1.0f; 192 | 193 | // 1.0 = Normal saturation; larger values produce more saturated colours; 194 | // 0.0 produces a greyscale image. 195 | float video_saturation = 1.0f; 196 | 197 | // 0.0 means no sharpening 198 | float video_sharpness = 0.0f; 199 | 200 | // HDR mode for Camera Module 3 201 | bool video_hdr = true; 202 | 203 | // The default is to initiate autofocus at any moment 204 | char video_autofocus_mode[11] = "continuous"; 205 | 206 | // -1.0f means lens position is not specified. 207 | float video_lens_position = -1.0f; 208 | 209 | char state_dir[256] = "state"; 210 | char hooks_dir[256] = "hooks"; 211 | float audio_volume_multiply = 1.0f; 212 | int audio_min_value = SHRT_MIN; // -32768 213 | int audio_max_value = SHRT_MAX; // 32767 214 | 215 | // Noise Gate 216 | float ng_thresh_volume = 1.0f; 217 | float ng_attack_time = 0.2f; 218 | float ng_hold_time = 1.0f; 219 | float ng_release_time = 0.5f; 220 | 221 | int is_hls_encryption_enabled = 0; 222 | char hls_encryption_key_uri[256] = "stream.key"; 223 | uint8_t hls_encryption_key[16] = { 224 | 0x75, 0xb0, 0xa8, 0x1d, 0xe1, 0x74, 0x87, 0xc8, 225 | 0x8a, 0x47, 0x50, 0x7a, 0x7e, 0x1f, 0xdf, 0x73, 226 | }; 227 | uint8_t hls_encryption_iv[16] = { 228 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 229 | 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 230 | }; 231 | int is_preview_enabled = 0; 232 | int is_previewrect_enabled = 0; 233 | int preview_x = 0; 234 | int preview_y = 0; 235 | int preview_width = 0; 236 | int preview_height = 0; 237 | int preview_opacity = 255; 238 | int preview_hdmi = 0; 239 | uint32_t blank_background_color = 0; 240 | int record_buffer_keyframes = 5; 241 | 242 | int is_timestamp_enabled = 0; 243 | char timestamp_format[128] = "%a %b %d %l:%M:%S %p"; 244 | LAYOUT_ALIGN timestamp_layout = (LAYOUT_ALIGN)(LAYOUT_ALIGN_BOTTOM | LAYOUT_ALIGN_RIGHT); 245 | int timestamp_horizontal_margin = 10; 246 | int timestamp_vertical_margin = 10; 247 | int timestamp_pos_x = 0; 248 | int timestamp_pos_y = 0; 249 | int is_timestamp_abs_pos_enabled = 0; 250 | TEXT_ALIGN timestamp_text_align = TEXT_ALIGN_LEFT; 251 | char timestamp_font_name[128] = "FreeMono:style=Bold"; 252 | char timestamp_font_file[1024] = ""; 253 | int timestamp_font_face_index = 0; 254 | float timestamp_font_points = 14.0f; 255 | int timestamp_font_dpi = 96; 256 | int timestamp_color = 0xffffff; 257 | int timestamp_stroke_color = 0x000000; 258 | float timestamp_stroke_width = 1.3f; 259 | int timestamp_letter_spacing = 0; 260 | bool show_version = false; 261 | bool show_help = false; 262 | 263 | private: 264 | void calculate(); 265 | }; 266 | -------------------------------------------------------------------------------- /preview/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | pkg_check_modules(LIBDRM QUIET libdrm) 6 | pkg_check_modules(X11 QUIET x11) 7 | pkg_check_modules(EPOXY QUIET epoxy) 8 | #pkg_check_modules(QTCORE QUIET Qt5Core) 9 | #pkg_check_modules(QTWIDGETS QUIET Qt5Widgets) 10 | 11 | set(SRC "preview.cpp") 12 | set(TARGET_LIBS "") 13 | 14 | IF (NOT DEFINED ENABLE_DRM) 15 | SET(ENABLE_DRM 1) 16 | endif() 17 | set(DRM_FOUND 0) 18 | if (ENABLE_DRM AND LIBDRM_FOUND) 19 | message(STATUS "LIBDRM_LINK_LIBRARIES=${LIBDRM_LINK_LIBRARIES}") 20 | include_directories(${LIBDRM_INCLUDE_DIRS}) 21 | set(TARGET_LIBS ${TARGET_LIBS} ${LIBDRM_LIBRARIES}) 22 | set(SRC ${SRC} drm_preview.cpp) 23 | set(DRM_FOUND 1) 24 | message(STATUS "LIBDRM display mode enabled") 25 | else() 26 | message(STATUS "LIBDRM display mode will be unavailable!") 27 | endif() 28 | 29 | IF (NOT DEFINED ENABLE_X11) 30 | set(ENABLE_X11 1) 31 | endif() 32 | set(EGL_FOUND 0) 33 | if (ENABLE_X11 AND X11_FOUND AND EPOXY_FOUND) 34 | message(STATUS "X11_LINK_LIBRARIES=${X11_LINK_LIBRARIES}") 35 | message(STATUS "EPOXY_LINK_LIBRARIES=${EPOXY_LINK_LIBRARIES}") 36 | set(TARGET_LIBS ${TARGET_LIBS} ${X11_LIBRARIES} ${EPOXY_LIBRARIES}) 37 | set(SRC ${SRC} egl_preview.cpp) 38 | set(EGL_FOUND 1) 39 | message(STATUS "EGL display mode enabled") 40 | else() 41 | message(STATUS "EGL display mode will be unavailable!") 42 | endif() 43 | 44 | #IF (NOT DEFINED ENABLE_QT) 45 | # message(STATUS "ENABLE_QT not specified - set to 1") 46 | # set(ENABLE_QT 1) 47 | #endif() 48 | set(QT_FOUND 0) 49 | #if (ENABLE_QT AND QTCORE_FOUND AND QTWIDGETS_FOUND) 50 | # message(STATUS "QTCORE_LINK_LIBRARIES=${QTCORE_LINK_LIBRARIES}") 51 | # message(STATUS "QTCORE_INCLUDE_DIRS=${QTCORE_INCLUDE_DIRS}") 52 | # include_directories(${QTCORE_INCLUDE_DIRS} ${QTWIDGETS_INCLUDE_DIRS}) 53 | # set(TARGET_LIBS ${TARGET_LIBS} ${QTCORE_LIBRARIES} ${QTWIDGETS_LIBRARIES}) 54 | # # The qt5/QtCore/qvariant.h header throws a warning, so suppress this. 55 | # # Annoyingly there are two different (incompatible) flags for clang < 10 56 | # # and >= 10, so set both, and supress unknown options warnings. 57 | # set_source_files_properties(qt_preview.cpp PROPERTIES COMPILE_FLAGS 58 | # "-Wno-unknown-warning-option -Wno-deprecated-copy -Wno-deprecated") 59 | # set(SRC ${SRC} qt_preview.cpp) 60 | # set(QT_FOUND 1) 61 | # message(STATUS "QT display mode enabled") 62 | #else() 63 | # message(STATUS "QT display mode will be unavailable!") 64 | #endif() 65 | 66 | add_library(preview STATIC null_preview.cpp ${SRC}) 67 | target_link_libraries(preview ${TARGET_LIBS}) 68 | 69 | target_compile_definitions(preview PUBLIC LIBDRM_PRESENT=${DRM_FOUND}) 70 | target_compile_definitions(preview PUBLIC LIBEGL_PRESENT=${EGL_FOUND}) 71 | target_compile_definitions(preview PUBLIC QT_PRESENT=${QT_FOUND}) 72 | 73 | install(TARGETS preview LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 74 | 75 | -------------------------------------------------------------------------------- /preview/drm_preview.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. 4 | * 5 | * drm_preview.cpp - DRM-based preview window. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "log/log.h" 19 | 20 | #include "preview.hpp" 21 | 22 | class DrmPreview : public Preview 23 | { 24 | public: 25 | DrmPreview(PicamOption const *options); 26 | ~DrmPreview(); 27 | // Display the buffer. You get given the fd back in the BufferDoneCallback 28 | // once its available for re-use. 29 | virtual void Show(int fd, libcamera::Span span, StreamInfo const &info) override; 30 | // Reset the preview window, clearing the current buffers and being ready to 31 | // show new ones. 32 | virtual void Reset() override; 33 | // Return the maximum image size allowed. 34 | virtual void MaxImageSize(unsigned int &w, unsigned int &h) const override 35 | { 36 | w = max_image_width_; 37 | h = max_image_height_; 38 | } 39 | 40 | private: 41 | struct Buffer 42 | { 43 | Buffer() : fd(-1) {} 44 | int fd; 45 | size_t size; 46 | StreamInfo info; 47 | uint32_t bo_handle; 48 | unsigned int fb_handle; 49 | }; 50 | void makeBuffer(int fd, size_t size, StreamInfo const &info, Buffer &buffer); 51 | void findCrtc(); 52 | void findPlane(); 53 | int drmfd_; 54 | int conId_; 55 | uint32_t crtcId_; 56 | int crtcIdx_; 57 | uint32_t planeId_; 58 | unsigned int out_fourcc_; 59 | unsigned int x_; 60 | unsigned int y_; 61 | unsigned int width_; 62 | unsigned int height_; 63 | unsigned int screen_width_; 64 | unsigned int screen_height_; 65 | std::map buffers_; // map the DMABUF's fd to the Buffer 66 | int last_fd_; 67 | unsigned int max_image_width_; 68 | unsigned int max_image_height_; 69 | bool first_time_; 70 | }; 71 | 72 | #define ERRSTR strerror(errno) 73 | 74 | void DrmPreview::findCrtc() 75 | { 76 | int i; 77 | drmModeRes *res = drmModeGetResources(drmfd_); 78 | if (!res) 79 | throw std::runtime_error("drmModeGetResources failed: " + std::string(ERRSTR)); 80 | 81 | if (res->count_crtcs <= 0) 82 | throw std::runtime_error("drm: no crts"); 83 | 84 | max_image_width_ = res->max_width; 85 | max_image_height_ = res->max_height; 86 | log_debug("preview: count_crtcs=%d count_encoders=%d count_connectors=%d count_fbs=%d max_image_width=%u max_image_height=%u\n", 87 | res->count_crtcs, res->count_encoders, res->count_connectors, res->count_fbs, 88 | max_image_width_, max_image_height_); 89 | 90 | if (!conId_) 91 | { 92 | for (i = 0; i < res->count_connectors; i++) 93 | { 94 | if (i == this->options_->preview_hdmi) { 95 | log_debug("preview: CRTC connector %d: chosen\n", i); 96 | } else { 97 | log_debug("preview: CRTC connector %d: skipped because preview_hdmi=%d is specified\n", i, this->options_->preview_hdmi); 98 | continue; 99 | } 100 | log_debug("preview: inspecting connector: %d\n", i); 101 | drmModeConnector *con = drmModeGetConnector(drmfd_, res->connectors[i]); 102 | drmModeEncoder *enc = NULL; 103 | drmModeCrtc *crtc = NULL; 104 | 105 | if (con->encoder_id) 106 | { 107 | enc = drmModeGetEncoder(drmfd_, con->encoder_id); 108 | log_debug("preview: set enc\n"); 109 | if (enc->crtc_id) 110 | { 111 | crtc = drmModeGetCrtc(drmfd_, enc->crtc_id); 112 | log_debug("preview: set crtc\n"); 113 | } 114 | } 115 | 116 | if (!conId_ && crtc) 117 | { 118 | conId_ = con->connector_id; 119 | crtcId_ = crtc->crtc_id; 120 | log_debug("preview: set conId_=%d crtcId_=%u\n", conId_, crtcId_); 121 | } 122 | 123 | if (crtc) 124 | { 125 | screen_width_ = crtc->width; 126 | screen_height_ = crtc->height; 127 | log_debug("preview: crtc screen_width_=%u screen_height_=%u\n", screen_width_, screen_height_); 128 | } 129 | 130 | log_debug("Connector %u (crtc %u): type %u, %ux%u %s", 131 | con->connector_id, (crtc ? crtc->crtc_id : 0), con->connector_type, (crtc ? crtc->width : 0), (crtc ? crtc->height : 0), 132 | (conId_ == (int)con->connector_id ? " (chosen)" : "") 133 | ); 134 | } 135 | 136 | if (!conId_) 137 | throw std::runtime_error("No suitable enabled connector found"); 138 | } 139 | 140 | crtcIdx_ = -1; 141 | 142 | for (i = 0; i < res->count_crtcs; ++i) 143 | { 144 | if (crtcId_ == res->crtcs[i]) 145 | { 146 | crtcIdx_ = i; 147 | break; 148 | } 149 | } 150 | 151 | if (crtcIdx_ == -1) 152 | { 153 | drmModeFreeResources(res); 154 | throw std::runtime_error("drm: CRTC " + std::to_string(crtcId_) + " not found"); 155 | } 156 | 157 | if (res->count_connectors <= 0) 158 | { 159 | drmModeFreeResources(res); 160 | throw std::runtime_error("drm: no connectors"); 161 | } 162 | 163 | drmModeConnector *c; 164 | c = drmModeGetConnector(drmfd_, conId_); 165 | if (!c) 166 | { 167 | drmModeFreeResources(res); 168 | throw std::runtime_error("drmModeGetConnector failed: " + std::string(ERRSTR)); 169 | } 170 | 171 | if (!c->count_modes) 172 | { 173 | drmModeFreeConnector(c); 174 | drmModeFreeResources(res); 175 | throw std::runtime_error("connector supports no mode"); 176 | } 177 | 178 | if (!options_->is_previewrect_enabled || width_ == 0 || height_ == 0) 179 | { 180 | // fullscreen preview 181 | drmModeCrtc *crtc = drmModeGetCrtc(drmfd_, crtcId_); 182 | x_ = crtc->x; 183 | y_ = crtc->y; 184 | width_ = crtc->width; 185 | height_ = crtc->height; 186 | log_debug("preview: crtc x_=%u y_=%u width_=%u height_=%u\n", x_, y_, width_, height_); 187 | drmModeFreeCrtc(crtc); 188 | } 189 | } 190 | 191 | void DrmPreview::findPlane() 192 | { 193 | drmModePlaneResPtr planes; 194 | drmModePlanePtr plane; 195 | unsigned int i; 196 | unsigned int j; 197 | 198 | planes = drmModeGetPlaneResources(drmfd_); 199 | if (!planes) 200 | throw std::runtime_error("drmModeGetPlaneResources failed: " + std::string(ERRSTR)); 201 | 202 | try 203 | { 204 | for (i = 0; i < planes->count_planes; ++i) 205 | { 206 | plane = drmModeGetPlane(drmfd_, planes->planes[i]); 207 | if (!planes) 208 | throw std::runtime_error("drmModeGetPlane failed: " + std::string(ERRSTR)); 209 | 210 | if (!(plane->possible_crtcs & (1 << crtcIdx_))) 211 | { 212 | drmModeFreePlane(plane); 213 | continue; 214 | } 215 | 216 | for (j = 0; j < plane->count_formats; ++j) 217 | { 218 | if (plane->formats[j] == out_fourcc_) 219 | { 220 | break; 221 | } 222 | } 223 | 224 | if (j == plane->count_formats) 225 | { 226 | drmModeFreePlane(plane); 227 | continue; 228 | } 229 | 230 | planeId_ = plane->plane_id; 231 | 232 | drmModeFreePlane(plane); 233 | break; 234 | } 235 | } 236 | catch (std::exception const &e) 237 | { 238 | drmModeFreePlaneResources(planes); 239 | throw; 240 | } 241 | 242 | drmModeFreePlaneResources(planes); 243 | } 244 | 245 | DrmPreview::DrmPreview(PicamOption const *options) : Preview(options), last_fd_(-1), first_time_(true) 246 | { 247 | drmfd_ = drmOpen("vc4", NULL); 248 | if (drmfd_ < 0) 249 | throw std::runtime_error("drmOpen failed: " + std::string(ERRSTR)); 250 | 251 | x_ = options_->preview_x; 252 | y_ = options_->preview_y; 253 | width_ = options_->preview_width; 254 | height_ = options_->preview_height; 255 | screen_width_ = 0; 256 | screen_height_ = 0; 257 | log_debug("preview: ctor: x_=%u y_=%u width_=%u height_=%u\n", x_, y_, width_, height_); 258 | 259 | try 260 | { 261 | if (!drmIsMaster(drmfd_)) 262 | throw std::runtime_error("DRM preview unavailable - not master"); 263 | 264 | conId_ = 0; 265 | findCrtc(); 266 | out_fourcc_ = DRM_FORMAT_YUV420; 267 | findPlane(); 268 | } 269 | catch (std::exception const &e) 270 | { 271 | close(drmfd_); 272 | throw; 273 | } 274 | 275 | // Default behaviour here is to go fullscreen while maintaining aspect ratio. 276 | if (!options_->is_previewrect_enabled || width_ == 0 || height_ == 0 || x_ + width_ > screen_width_ || 277 | y_ + height_ > screen_height_) 278 | { 279 | x_ = y_ = 0; 280 | width_ = screen_width_; 281 | height_ = screen_height_; 282 | log_debug("preview: default behavior: x_=%u y_=%u width_=%u height_=%u\n", x_, y_, width_, height_); 283 | } 284 | } 285 | 286 | DrmPreview::~DrmPreview() 287 | { 288 | close(drmfd_); 289 | } 290 | 291 | // DRM doesn't seem to have userspace definitions of its enums, but the properties 292 | // contain enum-name-to-value tables. So the code below ends up using strings and 293 | // searching for name matches. I suppose it works... 294 | 295 | static void get_colour_space_info(std::optional const &cs, char const *&encoding, 296 | char const *&range) 297 | { 298 | static char const encoding_601[] = "601", encoding_709[] = "709"; 299 | static char const range_limited[] = "limited", range_full[] = "full"; 300 | encoding = encoding_601; 301 | range = range_limited; 302 | 303 | if (cs == libcamera::ColorSpace::Sycc) 304 | range = range_full; 305 | else if (cs == libcamera::ColorSpace::Smpte170m) 306 | /* all good */; 307 | else if (cs == libcamera::ColorSpace::Rec709) 308 | encoding = encoding_709; 309 | else 310 | std::cerr << "DrmPreview: unexpected colour space " << libcamera::ColorSpace::toString(cs) << std::endl; 311 | } 312 | 313 | [[maybe_unused]] static int drm_set_transparency(int fd, int plane_id, int transparency) 314 | { 315 | log_debug("drm_set_transparency: fd=%d plane_id=%d transparency=%d\n", fd, plane_id, transparency); 316 | char const *name = "alpha"; 317 | 318 | drmModeObjectPropertiesPtr properties = nullptr; 319 | drmModePropertyPtr prop = nullptr; 320 | int ret = -1; 321 | properties = drmModeObjectGetProperties(fd, plane_id, DRM_MODE_OBJECT_PLANE); 322 | 323 | for (unsigned int i = 0; i < properties->count_props; i++) 324 | { 325 | int prop_id = properties->props[i]; 326 | prop = drmModeGetProperty(fd, prop_id); 327 | if (!prop) 328 | continue; 329 | 330 | log_debug("prop[%d] name=%s flags=%d\n", i, prop->name, prop->flags); 331 | if (!strstr(prop->name, name)) 332 | { 333 | drmModeFreeProperty(prop); 334 | prop = nullptr; 335 | continue; 336 | } 337 | 338 | // We have found the right property from its name, now search the enum table 339 | // for the numerical value that corresponds to the value name that we have. 340 | log_debug("setting alpha to %d\n", transparency); 341 | ret = drmModeObjectSetProperty(fd, plane_id, DRM_MODE_OBJECT_PLANE, prop_id, transparency); 342 | if (ret < 0) 343 | std::cerr << "DrmPreview: failed to set value " << transparency << " for property " << name << std::endl; 344 | goto done; 345 | } 346 | 347 | std::cerr << "DrmPreview: failed to find property " << name << std::endl; 348 | done: 349 | if (prop) 350 | drmModeFreeProperty(prop); 351 | if (properties) 352 | drmModeFreeObjectProperties(properties); 353 | return ret; 354 | } 355 | 356 | static int drm_set_property(int fd, int plane_id, char const *name, char const *val) 357 | { 358 | drmModeObjectPropertiesPtr properties = nullptr; 359 | drmModePropertyPtr prop = nullptr; 360 | int ret = -1; 361 | properties = drmModeObjectGetProperties(fd, plane_id, DRM_MODE_OBJECT_PLANE); 362 | 363 | for (unsigned int i = 0; i < properties->count_props; i++) 364 | { 365 | int prop_id = properties->props[i]; 366 | prop = drmModeGetProperty(fd, prop_id); 367 | if (!prop) 368 | continue; 369 | 370 | if (!drm_property_type_is(prop, DRM_MODE_PROP_ENUM) || !strstr(prop->name, name)) 371 | { 372 | drmModeFreeProperty(prop); 373 | prop = nullptr; 374 | continue; 375 | } 376 | 377 | // We have found the right property from its name, now search the enum table 378 | // for the numerical value that corresponds to the value name that we have. 379 | for (int j = 0; j < prop->count_enums; j++) 380 | { 381 | if (!strstr(prop->enums[j].name, val)) 382 | continue; 383 | 384 | ret = drmModeObjectSetProperty(fd, plane_id, DRM_MODE_OBJECT_PLANE, prop_id, prop->enums[j].value); 385 | if (ret < 0) 386 | std::cerr << "DrmPreview: failed to set value " << val << " for property " << name << std::endl; 387 | goto done; 388 | } 389 | 390 | std::cerr << "DrmPreview: failed to find value " << val << " for property " << name << std::endl; 391 | goto done; 392 | } 393 | 394 | std::cerr << "DrmPreview: failed to find property " << name << std::endl; 395 | done: 396 | if (prop) 397 | drmModeFreeProperty(prop); 398 | if (properties) 399 | drmModeFreeObjectProperties(properties); 400 | return ret; 401 | } 402 | 403 | static void setup_colour_space(int fd, int plane_id, std::optional const &cs) 404 | { 405 | char const *encoding, *range; 406 | get_colour_space_info(cs, encoding, range); 407 | 408 | drm_set_property(fd, plane_id, "COLOR_ENCODING", encoding); 409 | drm_set_property(fd, plane_id, "COLOR_RANGE", range); 410 | } 411 | 412 | void DrmPreview::makeBuffer(int fd, size_t size, StreamInfo const &info, Buffer &buffer) 413 | { 414 | if (first_time_) 415 | { 416 | first_time_ = false; 417 | 418 | setup_colour_space(drmfd_, planeId_, info.colour_space); 419 | 420 | // This is not working (when executed, preview stops working until reboot) 421 | // if (this->options_->preview_opacity != 255) { 422 | // drm_set_transparency(drmfd_, planeId_, this->options_->preview_opacity); 423 | // } 424 | } 425 | 426 | buffer.fd = fd; 427 | buffer.size = size; 428 | buffer.info = info; 429 | 430 | if (drmPrimeFDToHandle(drmfd_, fd, &buffer.bo_handle)) 431 | throw std::runtime_error("drmPrimeFDToHandle failed for fd " + std::to_string(fd)); 432 | 433 | uint32_t offsets[4] = { 434 | 0, // start offset for Y plane 435 | info.stride * info.height, // start offset for U plane 436 | info.stride * info.height + (info.stride / 2) * (info.height / 2) // start offset for V plane 437 | }; 438 | uint32_t pitches[4] = { 439 | info.stride, // stride (== width) of Y plane 440 | info.stride / 2, // stride (== width) of U plane 441 | info.stride / 2 // stride (== width) of V plane 442 | }; 443 | uint32_t bo_handles[4] = { buffer.bo_handle, buffer.bo_handle, buffer.bo_handle }; 444 | 445 | if (drmModeAddFB2(drmfd_, info.width, info.height, out_fourcc_, bo_handles, pitches, offsets, &buffer.fb_handle, 0)) 446 | throw std::runtime_error("drmModeAddFB2 failed: " + std::string(ERRSTR)); 447 | } 448 | 449 | void DrmPreview::Show(int fd, libcamera::Span span, StreamInfo const &info) 450 | { 451 | Buffer &buffer = buffers_[fd]; 452 | if (buffer.fd == -1) { 453 | makeBuffer(fd, span.size(), info, buffer); 454 | } 455 | 456 | // info.width -> camera capture width 457 | // info.height -> camera capture height 458 | // info.stride -> camera capture stride 459 | // buffer.info.width -> frame width of this buffer 460 | // buffer.info.height -> frame height of this buffer 461 | // x_ -> requested preview offset x 462 | // y_ -> requested preview offset y 463 | // width_ -> requested preview width 464 | // height_ -> requested preview height 465 | 466 | int x_off = 0, y_off = 0; 467 | unsigned int w = width_, h = height_; 468 | 469 | unsigned int crtc_x; 470 | unsigned int crtc_y; 471 | if (x_off < 0 && x_off + (int)x_ < 0) { 472 | crtc_x = 0; 473 | } else { 474 | crtc_x = x_off + x_; 475 | } 476 | if (y_off < 0 && y_off + (int)y_ < 0) { 477 | crtc_y = 0; 478 | } else { 479 | crtc_y = y_off + y_; 480 | } 481 | if (drmModeSetPlane(drmfd_, planeId_, crtcId_, buffer.fb_handle, 482 | 0, // flags 483 | crtc_x, 484 | crtc_y, 485 | w, // crtc_w 486 | h, // crtc_h 487 | 0 << 16, // src_x in Q16.16 488 | 0 << 16, // src_y in Q16.16 489 | buffer.info.width << 16, // src_w in Q16.16 490 | buffer.info.height << 16 // src_h in Q16.16 491 | )) { 492 | throw std::runtime_error("drmModeSetPlane failed: " + std::string(ERRSTR)); 493 | } 494 | if (last_fd_ >= 0) { 495 | done_callback_(last_fd_); 496 | } 497 | last_fd_ = fd; 498 | } 499 | 500 | void DrmPreview::Reset() 501 | { 502 | for (auto &it : buffers_) 503 | { 504 | drmModeRmFB(drmfd_, it.second.fb_handle); 505 | // Apparently a "bo_handle" is a "gem" thing, and it needs closing. It feels like there 506 | // ought be an API to match "drmPrimeFDToHandle" for this, but I can only find an ioctl. 507 | drm_gem_close gem_close = {}; 508 | gem_close.handle = it.second.bo_handle; 509 | if (drmIoctl(drmfd_, DRM_IOCTL_GEM_CLOSE, &gem_close) < 0) 510 | // I have no idea what this would mean, so complain and try to carry on... 511 | std::cerr << "DRM_IOCTL_GEM_CLOSE failed" << std::endl; 512 | } 513 | buffers_.clear(); 514 | last_fd_ = -1; 515 | first_time_ = true; 516 | } 517 | 518 | Preview *make_drm_preview(PicamOption const *options) 519 | { 520 | return new DrmPreview(options); 521 | } 522 | -------------------------------------------------------------------------------- /preview/egl_preview.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. 4 | * 5 | * egl_preview.cpp - X/EGL-based preview window. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // Include libcamera stuff before X11, as X11 #defines both Status and None 13 | // which upsets the libcamera headers. 14 | 15 | #include "preview.hpp" 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | // We don't use Status below, so we could consider #undefining it here. 22 | // We do use None, so if we had to #undefine it we could replace it by zero 23 | // in what follows below. 24 | 25 | #include 26 | #include 27 | 28 | class EglPreview : public Preview 29 | { 30 | public: 31 | EglPreview(PicamOption const *options); 32 | ~EglPreview(); 33 | virtual void SetInfoText(const std::string &text) override; 34 | // Display the buffer. You get given the fd back in the BufferDoneCallback 35 | // once its available for re-use. 36 | virtual void Show(int fd, libcamera::Span span, StreamInfo const &info) override; 37 | // Reset the preview window, clearing the current buffers and being ready to 38 | // show new ones. 39 | virtual void Reset() override; 40 | // Check if the window manager has closed the preview. 41 | virtual bool Quit() override; 42 | // Return the maximum image size allowed. 43 | virtual void MaxImageSize(unsigned int &w, unsigned int &h) const override 44 | { 45 | w = max_image_width_; 46 | h = max_image_height_; 47 | } 48 | 49 | private: 50 | struct Buffer 51 | { 52 | Buffer() : fd(-1) {} 53 | int fd; 54 | size_t size; 55 | StreamInfo info; 56 | GLuint texture; 57 | }; 58 | void makeWindow(char const *name); 59 | void makeBuffer(int fd, size_t size, StreamInfo const &info, Buffer &buffer); 60 | ::Display *display_; 61 | EGLDisplay egl_display_; 62 | Window window_; 63 | EGLContext egl_context_; 64 | EGLSurface egl_surface_; 65 | std::map buffers_; // map the DMABUF's fd to the Buffer 66 | int last_fd_; 67 | bool first_time_; 68 | Atom wm_delete_window_; 69 | // size of preview window 70 | int x_; 71 | int y_; 72 | int width_; 73 | int height_; 74 | unsigned int max_image_width_; 75 | unsigned int max_image_height_; 76 | }; 77 | 78 | static GLint compile_shader(GLenum target, const char *source) 79 | { 80 | GLuint s = glCreateShader(target); 81 | glShaderSource(s, 1, (const GLchar **)&source, NULL); 82 | glCompileShader(s); 83 | 84 | GLint ok; 85 | glGetShaderiv(s, GL_COMPILE_STATUS, &ok); 86 | 87 | if (!ok) 88 | { 89 | GLchar *info; 90 | GLint size; 91 | 92 | glGetShaderiv(s, GL_INFO_LOG_LENGTH, &size); 93 | info = (GLchar *)malloc(size); 94 | 95 | glGetShaderInfoLog(s, size, NULL, info); 96 | throw std::runtime_error("failed to compile shader: " + std::string(info) + "\nsource:\n" + 97 | std::string(source)); 98 | } 99 | 100 | return s; 101 | } 102 | 103 | static GLint link_program(GLint vs, GLint fs) 104 | { 105 | GLint prog = glCreateProgram(); 106 | glAttachShader(prog, vs); 107 | glAttachShader(prog, fs); 108 | glLinkProgram(prog); 109 | 110 | GLint ok; 111 | glGetProgramiv(prog, GL_LINK_STATUS, &ok); 112 | if (!ok) 113 | { 114 | /* Some drivers return a size of 1 for an empty log. This is the size 115 | * of a log that contains only a terminating NUL character. 116 | */ 117 | GLint size; 118 | GLchar *info = NULL; 119 | glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &size); 120 | if (size > 1) 121 | { 122 | info = (GLchar *)malloc(size); 123 | glGetProgramInfoLog(prog, size, NULL, info); 124 | } 125 | 126 | throw std::runtime_error("failed to link: " + std::string(info ? info : "")); 127 | } 128 | 129 | return prog; 130 | } 131 | 132 | static void gl_setup(int width, int height, int window_width, int window_height) 133 | { 134 | float w_factor = width / (float)window_width; 135 | float h_factor = height / (float)window_height; 136 | float max_dimension = std::max(w_factor, h_factor); 137 | w_factor /= max_dimension; 138 | h_factor /= max_dimension; 139 | char vs[256]; 140 | snprintf(vs, sizeof(vs), 141 | "attribute vec4 pos;\n" 142 | "varying vec2 texcoord;\n" 143 | "\n" 144 | "void main() {\n" 145 | " gl_Position = pos;\n" 146 | " texcoord.x = pos.x / %f + 0.5;\n" 147 | " texcoord.y = 0.5 - pos.y / %f;\n" 148 | "}\n", 149 | 2.0 * w_factor, 2.0 * h_factor); 150 | vs[sizeof(vs) - 1] = 0; 151 | GLint vs_s = compile_shader(GL_VERTEX_SHADER, vs); 152 | const char *fs = "#extension GL_OES_EGL_image_external : enable\n" 153 | "precision mediump float;\n" 154 | "uniform samplerExternalOES s;\n" 155 | "varying vec2 texcoord;\n" 156 | "void main() {\n" 157 | " gl_FragColor = texture2D(s, texcoord);\n" 158 | "}\n"; 159 | GLint fs_s = compile_shader(GL_FRAGMENT_SHADER, fs); 160 | GLint prog = link_program(vs_s, fs_s); 161 | 162 | glUseProgram(prog); 163 | 164 | static const float verts[] = { -w_factor, -h_factor, w_factor, -h_factor, w_factor, h_factor, -w_factor, h_factor }; 165 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, verts); 166 | glEnableVertexAttribArray(0); 167 | } 168 | 169 | EglPreview::EglPreview(PicamOption const *options) : Preview(options), last_fd_(-1), first_time_(true) 170 | { 171 | display_ = XOpenDisplay(NULL); 172 | if (!display_) 173 | throw std::runtime_error("Couldn't open X display"); 174 | 175 | egl_display_ = eglGetDisplay(display_); 176 | if (!egl_display_) 177 | throw std::runtime_error("eglGetDisplay() failed"); 178 | 179 | EGLint egl_major, egl_minor; 180 | 181 | if (!eglInitialize(egl_display_, &egl_major, &egl_minor)) 182 | throw std::runtime_error("eglInitialize() failed"); 183 | 184 | x_ = options_->preview_x; 185 | y_ = options_->preview_y; 186 | width_ = options_->preview_width; 187 | height_ = options_->preview_height; 188 | makeWindow("libcamera-app"); 189 | 190 | // gl_setup() has to happen later, once we're sure we're in the display thread. 191 | } 192 | 193 | EglPreview::~EglPreview() 194 | { 195 | EglPreview::Reset(); 196 | eglDestroyContext(egl_display_, egl_context_); 197 | } 198 | 199 | static void no_border(Display *display, Window window) 200 | { 201 | static const unsigned MWM_HINTS_DECORATIONS = (1 << 1); 202 | static const int PROP_MOTIF_WM_HINTS_ELEMENTS = 5; 203 | 204 | typedef struct 205 | { 206 | unsigned long flags; 207 | unsigned long functions; 208 | unsigned long decorations; 209 | long inputMode; 210 | unsigned long status; 211 | } PropMotifWmHints; 212 | 213 | PropMotifWmHints motif_hints; 214 | Atom prop, proptype; 215 | unsigned long flags = 0; 216 | 217 | /* setup the property */ 218 | motif_hints.flags = MWM_HINTS_DECORATIONS; 219 | motif_hints.decorations = flags; 220 | 221 | /* get the atom for the property */ 222 | prop = XInternAtom(display, "_MOTIF_WM_HINTS", True); 223 | if (!prop) 224 | { 225 | /* something went wrong! */ 226 | return; 227 | } 228 | 229 | /* not sure this is correct, seems to work, XA_WM_HINTS didn't work */ 230 | proptype = prop; 231 | 232 | XChangeProperty(display, window, /* display, window */ 233 | prop, proptype, /* property, type */ 234 | 32, /* format: 32-bit datums */ 235 | PropModeReplace, /* mode */ 236 | (unsigned char *)&motif_hints, /* data */ 237 | PROP_MOTIF_WM_HINTS_ELEMENTS /* nelements */ 238 | ); 239 | } 240 | 241 | void EglPreview::makeWindow(char const *name) 242 | { 243 | int screen_num = DefaultScreen(display_); 244 | XSetWindowAttributes attr; 245 | unsigned long mask; 246 | Window root = RootWindow(display_, screen_num); 247 | int screen_width = DisplayWidth(display_, screen_num); 248 | int screen_height = DisplayHeight(display_, screen_num); 249 | 250 | // Default behaviour here is to use a 1024x768 window. 251 | if (width_ == 0 || height_ == 0) 252 | { 253 | width_ = 1024; 254 | height_ = 768; 255 | } 256 | 257 | if (!options_->is_previewrect_enabled || x_ + width_ > screen_width || y_ + height_ > screen_height) 258 | { 259 | x_ = y_ = 0; 260 | width_ = DisplayWidth(display_, screen_num); 261 | height_ = DisplayHeight(display_, screen_num); 262 | } 263 | 264 | static const EGLint attribs[] = 265 | { 266 | EGL_RED_SIZE, 1, 267 | EGL_GREEN_SIZE, 1, 268 | EGL_BLUE_SIZE, 1, 269 | EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, 270 | EGL_NONE 271 | }; 272 | EGLConfig config; 273 | EGLint num_configs; 274 | if (!eglChooseConfig(egl_display_, attribs, &config, 1, &num_configs)) 275 | throw std::runtime_error("couldn't get an EGL visual config"); 276 | 277 | EGLint vid; 278 | if (!eglGetConfigAttrib(egl_display_, config, EGL_NATIVE_VISUAL_ID, &vid)) 279 | throw std::runtime_error("eglGetConfigAttrib() failed\n"); 280 | 281 | XVisualInfo visTemplate = {}; 282 | visTemplate.visualid = (VisualID)vid; 283 | int num_visuals; 284 | XVisualInfo *visinfo = XGetVisualInfo(display_, VisualIDMask, &visTemplate, &num_visuals); 285 | 286 | /* window attributes */ 287 | attr.background_pixel = 0; 288 | attr.border_pixel = 0; 289 | attr.colormap = XCreateColormap(display_, root, visinfo->visual, AllocNone); 290 | attr.event_mask = StructureNotifyMask | ExposureMask | KeyPressMask; 291 | /* XXX this is a bad way to get a borderless window! */ 292 | mask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask; 293 | 294 | window_ = XCreateWindow(display_, root, x_, y_, width_, height_, 0, visinfo->depth, InputOutput, visinfo->visual, 295 | mask, &attr); 296 | 297 | if (!options_->is_previewrect_enabled) { 298 | no_border(display_, window_); 299 | } 300 | 301 | /* set hints and properties */ 302 | { 303 | XSizeHints sizehints; 304 | sizehints.x = x_; 305 | sizehints.y = y_; 306 | sizehints.width = width_; 307 | sizehints.height = height_; 308 | sizehints.flags = USSize | USPosition; 309 | XSetNormalHints(display_, window_, &sizehints); 310 | XSetStandardProperties(display_, window_, name, name, None, (char **)NULL, 0, &sizehints); 311 | } 312 | 313 | eglBindAPI(EGL_OPENGL_ES_API); 314 | 315 | static const EGLint ctx_attribs[] = { 316 | EGL_CONTEXT_CLIENT_VERSION, 2, 317 | EGL_NONE 318 | }; 319 | egl_context_ = eglCreateContext(egl_display_, config, EGL_NO_CONTEXT, ctx_attribs); 320 | if (!egl_context_) 321 | throw std::runtime_error("eglCreateContext failed"); 322 | 323 | XFree(visinfo); 324 | 325 | XMapWindow(display_, window_); 326 | 327 | // This stops the window manager from closing the window, so we get an event instead. 328 | wm_delete_window_ = XInternAtom(display_, "WM_DELETE_WINDOW", False); 329 | XSetWMProtocols(display_, window_, &wm_delete_window_, 1); 330 | 331 | egl_surface_ = eglCreateWindowSurface(egl_display_, config, reinterpret_cast(window_), NULL); 332 | if (!egl_surface_) 333 | throw std::runtime_error("eglCreateWindowSurface failed"); 334 | 335 | // We have to do eglMakeCurrent in the thread where it will run, but we must do it 336 | // here temporarily so as to get the maximum texture size. 337 | eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context_); 338 | int max_texture_size = 0; 339 | glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size); 340 | max_image_width_ = max_image_height_ = max_texture_size; 341 | // This "undoes" the previous eglMakeCurrent. 342 | eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); 343 | } 344 | 345 | static void get_colour_space_info(std::optional const &cs, EGLint &encoding, EGLint &range) 346 | { 347 | encoding = EGL_ITU_REC601_EXT; 348 | range = EGL_YUV_NARROW_RANGE_EXT; 349 | 350 | if (cs == libcamera::ColorSpace::Sycc) 351 | range = EGL_YUV_FULL_RANGE_EXT; 352 | else if (cs == libcamera::ColorSpace::Smpte170m) 353 | /* all good */; 354 | else if (cs == libcamera::ColorSpace::Rec709) 355 | encoding = EGL_ITU_REC709_EXT; 356 | else 357 | std::cerr << "EglPreview: unexpected colour space " << libcamera::ColorSpace::toString(cs) << std::endl; 358 | } 359 | 360 | void EglPreview::makeBuffer(int fd, size_t size, StreamInfo const &info, Buffer &buffer) 361 | { 362 | if (first_time_) 363 | { 364 | // This stuff has to be delayed until we know we're in the thread doing the display. 365 | if (!eglMakeCurrent(egl_display_, egl_surface_, egl_surface_, egl_context_)) 366 | throw std::runtime_error("eglMakeCurrent failed"); 367 | gl_setup(info.width, info.height, width_, height_); 368 | first_time_ = false; 369 | } 370 | 371 | buffer.fd = fd; 372 | buffer.size = size; 373 | buffer.info = info; 374 | 375 | EGLint encoding, range; 376 | get_colour_space_info(info.colour_space, encoding, range); 377 | 378 | EGLint attribs[] = { 379 | EGL_WIDTH, static_cast(info.width), 380 | EGL_HEIGHT, static_cast(info.height), 381 | EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_YUV420, 382 | EGL_DMA_BUF_PLANE0_FD_EXT, fd, 383 | EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, 384 | EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast(info.stride), 385 | EGL_DMA_BUF_PLANE1_FD_EXT, fd, 386 | EGL_DMA_BUF_PLANE1_OFFSET_EXT, static_cast(info.stride * info.height), 387 | EGL_DMA_BUF_PLANE1_PITCH_EXT, static_cast(info.stride / 2), 388 | EGL_DMA_BUF_PLANE2_FD_EXT, fd, 389 | EGL_DMA_BUF_PLANE2_OFFSET_EXT, static_cast(info.stride * info.height + (info.stride / 2) * (info.height / 2)), 390 | EGL_DMA_BUF_PLANE2_PITCH_EXT, static_cast(info.stride / 2), 391 | EGL_YUV_COLOR_SPACE_HINT_EXT, encoding, 392 | EGL_SAMPLE_RANGE_HINT_EXT, range, 393 | EGL_NONE 394 | }; 395 | 396 | EGLImage image = eglCreateImageKHR(egl_display_, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs); 397 | if (!image) 398 | throw std::runtime_error("failed to import fd " + std::to_string(fd)); 399 | 400 | glGenTextures(1, &buffer.texture); 401 | glBindTexture(GL_TEXTURE_EXTERNAL_OES, buffer.texture); 402 | glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 403 | glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 404 | glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image); 405 | 406 | eglDestroyImageKHR(egl_display_, image); 407 | } 408 | 409 | void EglPreview::SetInfoText(const std::string &text) 410 | { 411 | if (!text.empty()) 412 | XStoreName(display_, window_, text.c_str()); 413 | } 414 | 415 | void EglPreview::Show(int fd, libcamera::Span span, StreamInfo const &info) 416 | { 417 | Buffer &buffer = buffers_[fd]; 418 | if (buffer.fd == -1) 419 | makeBuffer(fd, span.size(), info, buffer); 420 | 421 | glClearColor(0, 0, 0, 0); 422 | glClear(GL_COLOR_BUFFER_BIT); 423 | 424 | glBindTexture(GL_TEXTURE_EXTERNAL_OES, buffer.texture); 425 | glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 426 | EGLBoolean success [[maybe_unused]] = eglSwapBuffers(egl_display_, egl_surface_); 427 | if (last_fd_ >= 0) 428 | done_callback_(last_fd_); 429 | last_fd_ = fd; 430 | } 431 | 432 | void EglPreview::Reset() 433 | { 434 | for (auto &it : buffers_) 435 | glDeleteTextures(1, &it.second.texture); 436 | buffers_.clear(); 437 | last_fd_ = -1; 438 | eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); 439 | first_time_ = true; 440 | } 441 | 442 | bool EglPreview::Quit() 443 | { 444 | XEvent event; 445 | while (XCheckTypedWindowEvent(display_, window_, ClientMessage, &event)) 446 | { 447 | if (static_cast(event.xclient.data.l[0]) == wm_delete_window_) 448 | return true; 449 | } 450 | return false; 451 | } 452 | 453 | Preview *make_egl_preview(PicamOption const *options) 454 | { 455 | return new EglPreview(options); 456 | } 457 | -------------------------------------------------------------------------------- /preview/null_preview.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2021, Raspberry Pi (Trading) Ltd. 4 | * 5 | * null_preview.cpp - dummy "show nothing" preview window. 6 | */ 7 | 8 | #include 9 | 10 | #include "log/log.h" 11 | 12 | #include "preview.hpp" 13 | 14 | class NullPreview : public Preview 15 | { 16 | public: 17 | NullPreview(PicamOption const *options) : Preview(options) 18 | { 19 | log_debug("Running without preview window\n"); 20 | } 21 | ~NullPreview() {} 22 | // Display the buffer. You get given the fd back in the BufferDoneCallback 23 | // once its available for re-use. 24 | virtual void Show(int fd, libcamera::Span span, StreamInfo const &info) override 25 | { 26 | done_callback_(fd); 27 | } 28 | // Reset the preview window, clearing the current buffers and being ready to 29 | // show new ones. 30 | void Reset() override {} 31 | // Return the maximum image size allowed. Zeroes mean "no limit". 32 | virtual void MaxImageSize(unsigned int &w, unsigned int &h) const override { w = h = 0; } 33 | 34 | private: 35 | }; 36 | 37 | Preview *make_null_preview(PicamOption const *options) 38 | { 39 | return new NullPreview(options); 40 | } 41 | -------------------------------------------------------------------------------- /preview/preview.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2021, Raspberry Pi (Trading) Ltd. 4 | * 5 | * preview.cpp - preview window interface 6 | */ 7 | 8 | #include 9 | 10 | #include "log/log.h" 11 | 12 | #include "preview.hpp" 13 | 14 | Preview *make_null_preview(PicamOption const *options); 15 | Preview *make_egl_preview(PicamOption const *options); 16 | Preview *make_drm_preview(PicamOption const *options); 17 | // Preview *make_qt_preview(Options const *options); 18 | 19 | Preview *make_preview(PicamOption const *options) 20 | { 21 | if (!options->is_preview_enabled) { 22 | // Do not show preview 23 | return make_null_preview(options); 24 | } 25 | // #if QT_PRESENT 26 | // else if (options->qt_preview) 27 | // { 28 | // Preview *p = make_qt_preview(options); 29 | // if (p) 30 | // std::cerr << "Made QT preview window" << std::endl; 31 | // return p; 32 | // } 33 | // #endif 34 | else 35 | { 36 | try 37 | { 38 | #if LIBEGL_PRESENT 39 | // EGL is used when X Window System is running. 40 | // DRM cannot be used if X is running. 41 | Preview *p = make_egl_preview(options); 42 | if (p) { 43 | log_debug("Made X/EGL preview window\n"); 44 | } 45 | return p; 46 | #else 47 | throw std::runtime_error("egl libraries unavailable."); 48 | #endif 49 | } 50 | catch (std::exception const &e) 51 | { 52 | log_debug("make_egl_preview error: %s\n", e.what()); 53 | try 54 | { 55 | #if LIBDRM_PRESENT 56 | // DRM (Direct Rendering Mangaer) is used when X is not running. 57 | Preview *p = make_drm_preview(options); 58 | if (p) { 59 | log_debug("Made DRM preview window\n"); 60 | } 61 | return p; 62 | #else 63 | throw std::runtime_error("drm libraries unavailable."); 64 | #endif 65 | } 66 | catch (std::exception const &e) 67 | { 68 | std::cerr << "Preview window unavailable: " << e.what() << std::endl; 69 | return make_null_preview(options); 70 | } 71 | } 72 | } 73 | 74 | return nullptr; // prevents compiler warning in debug builds 75 | } 76 | -------------------------------------------------------------------------------- /preview/preview.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. 4 | * 5 | * preview.hpp - preview window interface 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "core/stream_info.hpp" 16 | #include "picam_option/picam_option.hpp" 17 | 18 | // struct Options; 19 | 20 | class Preview 21 | { 22 | public: 23 | typedef std::function DoneCallback; 24 | 25 | Preview(PicamOption const *options) : options_(options) {} 26 | virtual ~Preview() {} 27 | // This is where the application sets the callback it gets whenever the viewfinder 28 | // is no longer displaying the buffer and it can be safely recycled. 29 | void SetDoneCallback(DoneCallback callback) { done_callback_ = callback; } 30 | virtual void SetInfoText(const std::string &text) {} 31 | // Display the buffer. You get given the fd back in the BufferDoneCallback 32 | // once its available for re-use. 33 | virtual void Show(int fd, libcamera::Span span, StreamInfo const &info) = 0; 34 | // Reset the preview window, clearing the current buffers and being ready to 35 | // show new ones. 36 | virtual void Reset() = 0; 37 | // Check if preview window has been shut down. 38 | virtual bool Quit() { return false; } 39 | // Return the maximum image size allowed. 40 | virtual void MaxImageSize(unsigned int &w, unsigned int &h) const = 0; 41 | 42 | protected: 43 | DoneCallback done_callback_; 44 | PicamOption const *options_; 45 | }; 46 | 47 | Preview *make_preview(PicamOption const *options); 48 | -------------------------------------------------------------------------------- /rtsp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(rtsp STATIC rtsp.c) 6 | target_link_libraries(rtsp) 7 | 8 | install(TARGETS rtsp LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | -------------------------------------------------------------------------------- /rtsp/rtsp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "log/log.h" 10 | #include "rtsp.h" 11 | 12 | // UNIX domain sockets 13 | static int sockfd_video; 14 | static int sockfd_video_control; 15 | static int sockfd_audio; 16 | static int sockfd_audio_control; 17 | 18 | void rtsp_setup_socks(RtspConfig config) { 19 | struct sockaddr_un remote_video; 20 | struct sockaddr_un remote_audio; 21 | 22 | int len; 23 | struct sockaddr_un remote_video_control; 24 | struct sockaddr_un remote_audio_control; 25 | 26 | log_debug("connecting to UNIX domain sockets\n"); 27 | 28 | // Setup sockfd_video 29 | if ((sockfd_video = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 30 | perror("socket video"); 31 | exit(EXIT_FAILURE); 32 | } 33 | remote_video.sun_family = AF_UNIX; 34 | strcpy(remote_video.sun_path, config.rtsp_video_data_path); 35 | len = strlen(remote_video.sun_path) + sizeof(remote_video.sun_family); 36 | if (connect(sockfd_video, (struct sockaddr *)&remote_video, len) == -1) { 37 | log_error("error: failed to connect to video data socket (%s): %s\n" 38 | "perhaps RTSP server (https://github.com/iizukanao/node-rtsp-rtmp-server) is not running?\n", 39 | config.rtsp_video_data_path, strerror(errno)); 40 | exit(EXIT_FAILURE); 41 | } 42 | 43 | // Setup sockfd_video_control 44 | if ((sockfd_video_control = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 45 | perror("socket video_control"); 46 | exit(EXIT_FAILURE); 47 | } 48 | remote_video_control.sun_family = AF_UNIX; 49 | strcpy(remote_video_control.sun_path, config.rtsp_video_control_path); 50 | len = strlen(remote_video_control.sun_path) + sizeof(remote_video_control.sun_family); 51 | if (connect(sockfd_video_control, (struct sockaddr *)&remote_video_control, len) == -1) { 52 | log_error("error: failed to connect to video control socket (%s): %s\n" 53 | "perhaps RTSP server (https://github.com/iizukanao/node-rtsp-rtmp-server) is not running?\n", 54 | config.rtsp_video_control_path, strerror(errno)); 55 | exit(EXIT_FAILURE); 56 | } 57 | 58 | // Setup sockfd_audio 59 | if ((sockfd_audio = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 60 | perror("socket audio"); 61 | exit(EXIT_FAILURE); 62 | } 63 | remote_audio.sun_family = AF_UNIX; 64 | strcpy(remote_audio.sun_path, config.rtsp_audio_data_path); 65 | len = strlen(remote_audio.sun_path) + sizeof(remote_audio.sun_family); 66 | if (connect(sockfd_audio, (struct sockaddr *)&remote_audio, len) == -1) { 67 | log_error("error: failed to connect to audio data socket (%s): %s\n" 68 | "perhaps RTSP server (https://github.com/iizukanao/node-rtsp-rtmp-server) is not running?\n", 69 | config.rtsp_audio_data_path, strerror(errno)); 70 | exit(EXIT_FAILURE); 71 | } 72 | 73 | // Setup sockfd_audio_control 74 | if ((sockfd_audio_control = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { 75 | perror("socket audio_control"); 76 | exit(EXIT_FAILURE); 77 | } 78 | remote_audio_control.sun_family = AF_UNIX; 79 | strcpy(remote_audio_control.sun_path, config.rtsp_audio_control_path); 80 | len = strlen(remote_audio_control.sun_path) + sizeof(remote_audio_control.sun_family); 81 | if (connect(sockfd_audio_control, (struct sockaddr *)&remote_audio_control, len) == -1) { 82 | log_error("error: failed to connect to audio control socket (%s): %s\n" 83 | "perhaps RTSP server (https://github.com/iizukanao/node-rtsp-rtmp-server) is not running?\n", 84 | config.rtsp_audio_control_path, strerror(errno)); 85 | exit(EXIT_FAILURE); 86 | } 87 | } 88 | 89 | // Send video packet to node-rtsp-rtmp-server 90 | void rtsp_send_video_start_time() { 91 | int payload_size = 11; 92 | uint8_t sendbuf[14] = { 93 | // payload size 94 | (payload_size >> 16) & 0xff, 95 | (payload_size >> 8) & 0xff, 96 | payload_size & 0xff, 97 | 98 | // payload 99 | // packet type 100 | 0x00, 101 | // stream name 102 | 'l', 'i', 'v', 'e', '/', 'p', 'i', 'c', 'a', 'm', 103 | }; 104 | if (send(sockfd_video_control, sendbuf, sizeof(sendbuf), 0) == -1) { 105 | perror("send video start time"); 106 | exit(EXIT_FAILURE); 107 | } 108 | } 109 | 110 | // Send audio packet to node-rtsp-rtmp-server 111 | void rtsp_send_audio_start_time(int64_t audio_start_time) { 112 | int payload_size = 9; 113 | int64_t logical_start_time = audio_start_time; 114 | uint8_t sendbuf[12] = { 115 | // payload size 116 | (payload_size >> 16) & 0xff, 117 | (payload_size >> 8) & 0xff, 118 | payload_size & 0xff, 119 | // packet type (0x01 == audio start time) 120 | 0x01, 121 | // payload 122 | logical_start_time >> 56, 123 | (logical_start_time >> 48) & 0xff, 124 | (logical_start_time >> 40) & 0xff, 125 | (logical_start_time >> 32) & 0xff, 126 | (logical_start_time >> 24) & 0xff, 127 | (logical_start_time >> 16) & 0xff, 128 | (logical_start_time >> 8) & 0xff, 129 | logical_start_time & 0xff, 130 | }; 131 | if (send(sockfd_audio_control, sendbuf, 12, 0) == -1) { 132 | perror("send audio start time"); 133 | exit(EXIT_FAILURE); 134 | } 135 | } 136 | 137 | void rtsp_send_audio_frame(uint8_t *databuf, int databuflen, int64_t pts) { 138 | int payload_size = databuflen + 7; // +1(packet type) +6(pts) 139 | int total_size = payload_size + 3; // more 3 bytes for payload length 140 | uint8_t *sendbuf = malloc(total_size); 141 | if (sendbuf == NULL) { 142 | log_error("error: cannot allocate memory for audio sendbuf: size=%d", total_size); 143 | return; 144 | } 145 | // payload header 146 | sendbuf[0] = (payload_size >> 16) & 0xff; 147 | sendbuf[1] = (payload_size >> 8) & 0xff; 148 | sendbuf[2] = payload_size & 0xff; 149 | // payload 150 | sendbuf[3] = 0x03; // packet type (0x03 == audio data) 151 | sendbuf[4] = (pts >> 40) & 0xff; 152 | sendbuf[5] = (pts >> 32) & 0xff; 153 | sendbuf[6] = (pts >> 24) & 0xff; 154 | sendbuf[7] = (pts >> 16) & 0xff; 155 | sendbuf[8] = (pts >> 8) & 0xff; 156 | sendbuf[9] = pts & 0xff; 157 | memcpy(sendbuf + 10, databuf, databuflen); 158 | if (send(sockfd_audio, sendbuf, total_size, 0) == -1) { 159 | perror("send audio data"); 160 | } 161 | free(sendbuf); 162 | } 163 | 164 | void rtsp_send_video_frame(uint8_t *databuf, int databuflen, int64_t pts) { 165 | int payload_size = databuflen + 7; // +1(packet type) +6(pts) 166 | int total_size = payload_size + 3; // more 3 bytes for payload length 167 | uint8_t *sendbuf = malloc(total_size); 168 | if (sendbuf == NULL) { 169 | log_error("error: cannot allocate memory for video sendbuf: size=%d", total_size); 170 | return; 171 | } 172 | // payload header 173 | sendbuf[0] = (payload_size >> 16) & 0xff; 174 | sendbuf[1] = (payload_size >> 8) & 0xff; 175 | sendbuf[2] = payload_size & 0xff; 176 | // payload 177 | sendbuf[3] = 0x02; // packet type (0x02 == video data) 178 | sendbuf[4] = (pts >> 40) & 0xff; 179 | sendbuf[5] = (pts >> 32) & 0xff; 180 | sendbuf[6] = (pts >> 24) & 0xff; 181 | sendbuf[7] = (pts >> 16) & 0xff; 182 | sendbuf[8] = (pts >> 8) & 0xff; 183 | sendbuf[9] = pts & 0xff; 184 | memcpy(sendbuf + 10, databuf, databuflen); 185 | if (send(sockfd_video, sendbuf, total_size, 0) == -1) { 186 | perror("send video data"); 187 | } 188 | free(sendbuf); 189 | } 190 | 191 | void rtsp_teardown_socks() { 192 | close(sockfd_video); 193 | close(sockfd_video_control); 194 | close(sockfd_audio); 195 | close(sockfd_audio_control); 196 | } 197 | -------------------------------------------------------------------------------- /rtsp/rtsp.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef _PICAM_RTSP_H_ 6 | #define _PICAM_RTSP_H_ 7 | 8 | typedef struct RtspConfig { 9 | char *rtsp_video_control_path; 10 | char *rtsp_audio_control_path; 11 | char *rtsp_video_data_path; 12 | char *rtsp_audio_data_path; 13 | } RtspConfig; 14 | 15 | void rtsp_setup_socks(RtspConfig config); 16 | void rtsp_teardown_socks(); 17 | void rtsp_send_video_start_time(); 18 | void rtsp_send_audio_start_time(int64_t audio_start_time); 19 | void rtsp_send_video_frame(uint8_t *databuf, int databuflen, int64_t pts); 20 | void rtsp_send_audio_frame(uint8_t *databuf, int databuflen, int64_t pts); 21 | 22 | #endif 23 | 24 | #ifdef __cplusplus 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /subtitle/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(subtitle STATIC subtitle.c) 6 | target_link_libraries(subtitle text) 7 | 8 | install(TARGETS subtitle LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /subtitle/subtitle.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../text/text.h" 7 | #include "subtitle.h" 8 | 9 | static const char *default_font_name = "sans"; 10 | 11 | static int text_id = -1; 12 | static int64_t hide_time = 0; 13 | 14 | /** 15 | * Initializes the subtitle library with a font name. 16 | */ 17 | void subtitle_init_with_font_name(const char *font_name, int points, int dpi) { 18 | char *font_file; 19 | int face_index; 20 | 21 | if (font_name != NULL) { 22 | text_select_font_file(font_name, &font_file, &face_index); 23 | } else { 24 | text_select_font_file(default_font_name, &font_file, &face_index); 25 | } 26 | subtitle_init(font_file, face_index, points, dpi); 27 | free(font_file); 28 | } 29 | 30 | /** 31 | * Initializes the subtitle library with a font file and face index. 32 | * 33 | * Previous text created by subtitle_init() will be destroyed. 34 | */ 35 | void subtitle_init(const char *font_file, long face_index, int points, int dpi) { 36 | int old_text_id = text_id; 37 | 38 | text_id = text_create( 39 | font_file, face_index, 40 | points, 41 | dpi 42 | ); 43 | text_set_stroke_color(text_id, 0x000000); 44 | text_set_letter_spacing(text_id, 1); 45 | text_set_color(text_id, 0xffffff); 46 | text_set_layout(text_id, 47 | LAYOUT_ALIGN_BOTTOM | LAYOUT_ALIGN_CENTER, // layout alignment for the box 48 | 0, // horizontal margin from the right edge 49 | 30); // vertical margin from the bottom edge 50 | text_set_align(text_id, TEXT_ALIGN_CENTER); // text alignment inside the box 51 | 52 | if (old_text_id != -1) { 53 | // Queue old_text_id to be destroyed when text_id will appear on screen 54 | text_destroy_on_appear(old_text_id, text_id); 55 | } 56 | } 57 | 58 | /** 59 | * Destroys the resources used by subtitle library. 60 | */ 61 | void subtitle_shutdown() { 62 | if (text_id != -1) { 63 | text_destroy(text_id); 64 | text_id = -1; 65 | } 66 | } 67 | 68 | /** 69 | * Sets text color. 70 | */ 71 | void subtitle_set_color(int color) { 72 | text_set_color(text_id, color); 73 | } 74 | 75 | /** 76 | * Sets text visibility 77 | */ 78 | void subtitle_set_visibility(int in_preview, int in_video) { 79 | text_set_visibility(text_id, in_preview, in_video); 80 | } 81 | 82 | /** 83 | * Sets text stroke color. 84 | */ 85 | void subtitle_set_stroke_color(uint32_t color) { 86 | text_set_stroke_color(text_id, color); 87 | } 88 | 89 | /** 90 | * Sets text stroke border width in points. 91 | */ 92 | void subtitle_set_stroke_width(float stroke_width) { 93 | text_set_stroke_width(text_id, stroke_width); 94 | } 95 | 96 | /** 97 | * Sets letter spacing in pixels. 98 | */ 99 | void subtitle_set_letter_spacing(int letter_spacing) { 100 | text_set_letter_spacing(text_id, letter_spacing); 101 | } 102 | 103 | /** 104 | * Sets multiplying factor for line spacing. 105 | * If this is set to 1, default line spacing is used. 106 | */ 107 | void subtitle_set_line_height_multiply(float multiply) { 108 | text_set_line_height_multiply(text_id, multiply); 109 | } 110 | 111 | /** 112 | * Sets the scale of a tab (\t) character. 113 | * Tab width will be multiplied by the given number. 114 | */ 115 | void subtitle_set_tab_scale(float multiply) { 116 | text_set_tab_scale(text_id, multiply); 117 | } 118 | 119 | /** 120 | * Sets the absolute position for the subtitle. 121 | */ 122 | void subtitle_set_position(int x, int y) { 123 | text_set_position(text_id, x, y); 124 | } 125 | 126 | /** 127 | * Sets the relative layout for the text in the screen. 128 | */ 129 | void subtitle_set_layout(LAYOUT_ALIGN layout_align, int horizontal_margin, int vertical_margin) { 130 | text_set_layout(text_id, layout_align, horizontal_margin, vertical_margin); 131 | } 132 | 133 | /** 134 | * Sets the text alignment inside a positioned box. 135 | */ 136 | void subtitle_set_align(TEXT_ALIGN text_align) { 137 | text_set_align(text_id, text_align); 138 | } 139 | 140 | /** 141 | * Call this function before calling text_draw_all(). 142 | */ 143 | void subtitle_update() { 144 | struct timespec ts; 145 | 146 | if (hide_time > 0) { 147 | clock_gettime(CLOCK_MONOTONIC, &ts); 148 | int64_t current_time = ts.tv_sec * INT64_C(1000000000) + ts.tv_nsec; 149 | if (current_time > hide_time) { 150 | text_clear(text_id); 151 | hide_time = 0; 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Show subtitle text for duration_sec. 158 | * When duration_sec is 0, the text will be displayed indefinitely. 159 | */ 160 | void subtitle_show(const char *text, size_t text_len, float duration_sec) { 161 | struct timespec ts; 162 | 163 | text_set_text(text_id, text, text_len); 164 | redraw_text(text_id); 165 | 166 | if (duration_sec > 0.0f) { 167 | // hide the text after duration_sec 168 | clock_gettime(CLOCK_MONOTONIC, &ts); 169 | hide_time = ts.tv_sec * INT64_C(1000000000) + 170 | ts.tv_nsec + duration_sec * INT64_C(1000000000); 171 | } else { 172 | // show the text indefinitely 173 | hide_time = 0; 174 | } 175 | } 176 | 177 | /** 178 | * Hide the subtitle. 179 | * 180 | * This will not destroy the resources used by the text. 181 | * To destroy the resources, call subtitle_shutdown(). 182 | */ 183 | void subtitle_clear() { 184 | if (text_id != -1) { 185 | text_clear(text_id); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /subtitle/subtitle.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef PICAM_SUBTITLE_H 6 | #define PICAM_SUBTITLE_H 7 | 8 | /** 9 | * Initializes the timestamp library with a font name. 10 | */ 11 | void subtitle_init_with_font_name(const char *font_name, int points, int dpi); 12 | 13 | /** 14 | * Initializes the timestamp library with a font file and face index. 15 | */ 16 | void subtitle_init(const char *font_file, long face_index, int points, int dpi); 17 | 18 | /** 19 | * Destroys the resources used by timestamp library. 20 | */ 21 | void subtitle_shutdown(); 22 | 23 | /** 24 | * Sets text color. 25 | */ 26 | void subtitle_set_color(int color); 27 | 28 | /** 29 | * Sets text visibility 30 | */ 31 | void subtitle_set_visibility(int in_preview, int in_video); 32 | 33 | /** 34 | * Sets text stroke color. 35 | */ 36 | void subtitle_set_stroke_color(uint32_t color); 37 | 38 | /** 39 | * Sets text stroke border width in points. 40 | */ 41 | void subtitle_set_stroke_width(float stroke_width); 42 | 43 | /** 44 | * Sets letter spacing in pixels. 45 | */ 46 | void subtitle_set_letter_spacing(int letter_spacing); 47 | 48 | /** 49 | * Sets multiplying factor for line spacing. 50 | * If this is set to 1, default line spacing is used. 51 | */ 52 | void subtitle_set_line_height_multiply(float line_height_multiply); 53 | 54 | /** 55 | * Sets the scale of a tab (\t) character. 56 | * Tab width will be multiplied by the given number. 57 | */ 58 | void subtitle_set_tab_scale(float tab_scale); 59 | 60 | /** 61 | * Sets the absolute position for the timestamp. 62 | */ 63 | void subtitle_set_position(int x, int y); 64 | 65 | /** 66 | * Sets the relative layout for the text in the screen. 67 | */ 68 | void subtitle_set_layout(LAYOUT_ALIGN layout_align, int horizontal_margin, int vertical_margin); 69 | 70 | /** 71 | * Sets the text alignment inside a positioned box. 72 | */ 73 | void subtitle_set_align(TEXT_ALIGN text_align); 74 | 75 | /** 76 | * Call this function before calling text_draw_all(). 77 | */ 78 | void subtitle_update(); 79 | 80 | /** 81 | * Show subtitle text for duration_sec. 82 | */ 83 | void subtitle_show(const char *text, size_t text_len, float duration_sec); 84 | 85 | /** 86 | * Hide the subtitle. 87 | */ 88 | void subtitle_clear(); 89 | 90 | #endif // PICAM_SUBTITLE_H 91 | 92 | #ifdef __cplusplus 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /text/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(text STATIC text.c) 6 | target_link_libraries(text log fontconfig harfbuzz freetype) 7 | 8 | install(TARGETS text LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /text/text.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef PICAM_TEXT_H 6 | #define PICAM_TEXT_H 7 | 8 | #include 9 | 10 | // layout alignment in screen 11 | typedef enum LAYOUT_ALIGN { 12 | // horizontal align bitmask group 13 | LAYOUT_ALIGN_LEFT = 1, 14 | LAYOUT_ALIGN_CENTER = 2, 15 | LAYOUT_ALIGN_RIGHT = 3, 16 | LAYOUT_ALIGN_HORIZONTAL_MASK = 3, 17 | 18 | // vertical align bitmask group 19 | LAYOUT_ALIGN_TOP = 4, 20 | LAYOUT_ALIGN_MIDDLE = 8, 21 | LAYOUT_ALIGN_BOTTOM = 12, 22 | LAYOUT_ALIGN_VERTICAL_MASK = 12, 23 | } LAYOUT_ALIGN; 24 | 25 | // text alignment inside bounding box 26 | typedef enum TEXT_ALIGN { 27 | TEXT_ALIGN_LEFT = 1, 28 | TEXT_ALIGN_CENTER = 2, 29 | TEXT_ALIGN_RIGHT = 3, 30 | } TEXT_ALIGN; 31 | 32 | // Represents a bounding box for the text 33 | typedef struct text_bounds { 34 | int left; 35 | int right; 36 | int top; 37 | int bottom; 38 | int width; 39 | int height; 40 | } text_bounds; 41 | 42 | /** 43 | * Initializes text library. 44 | */ 45 | void text_init(); 46 | 47 | /** 48 | * Destroys resources used by text library. 49 | */ 50 | void text_teardown(); 51 | 52 | /** 53 | * Creates a new text object and returns the text id. 54 | */ 55 | int text_create(const char *font_file, long face_index, float point, int dpi); 56 | 57 | /** 58 | * Sets letter spacing. 59 | */ 60 | int text_set_letter_spacing(int text_id, int pixels); 61 | 62 | /** 63 | * Sets stroke color. 64 | */ 65 | int text_set_stroke_color(int text_id, uint32_t color); 66 | 67 | /** 68 | * Sets stroke border width in points. 69 | */ 70 | int text_set_stroke_width(int text_id, float stroke_width); 71 | 72 | /** 73 | * Sets text fill color. 74 | */ 75 | int text_set_color(int text_id, int color); 76 | 77 | /** 78 | * Sets text visibility 79 | */ 80 | int text_set_visibility(int text_id, int in_preview, int in_video); 81 | 82 | /** 83 | * Sets multiplying factor for line spacing. 84 | * If this is set to 1, default line spacing is used. 85 | */ 86 | int text_set_line_height_multiply(int text_id, float multiply); 87 | 88 | /** 89 | * Sets the scale of a tab (\t) character. 90 | * Tab width will be multiplied by the given number. 91 | */ 92 | int text_set_tab_scale(int text_id, float multiply); 93 | 94 | /** 95 | * Returns the default line spacing in pixels. 96 | */ 97 | float text_get_line_height(int text_id); 98 | 99 | /** 100 | * Returns the default ascender (distance from baseline to top) 101 | * in pixels. 102 | */ 103 | float text_get_ascender(int text_id); 104 | 105 | /** 106 | * Sets the absolute position for the text. 107 | */ 108 | int text_set_position(int text_id, int x, int y); 109 | 110 | /** 111 | * Sets the relative layout for the text in the screen. 112 | */ 113 | int text_set_layout(int text_id, LAYOUT_ALIGN layout_align, int horizontal_margin, int vertical_margin); 114 | 115 | /** 116 | * Sets the absolute position for the text based on the current 117 | * relative layout and canvas size. 118 | */ 119 | int text_fix_position(int text_id, int canvas_width, int canvas_height); 120 | 121 | /** 122 | * Sets the text alignment inside a positioned box. 123 | */ 124 | int text_set_align(int text_id, TEXT_ALIGN text_align); 125 | 126 | /** 127 | * Sets the text in UTF-8 encoded chars. 128 | */ 129 | int text_set_text(int text_id, const char *utf8_text, const size_t text_bytes); 130 | 131 | /** 132 | * Destroys the text object. 133 | */ 134 | int text_destroy(int text_id); 135 | 136 | /** 137 | * Calculates a bounding box for the text object. 138 | */ 139 | int text_get_bounds(int text_id, const char *text, size_t text_len, text_bounds *bounds); 140 | 141 | /** 142 | * Draw glyphs to internal buffer. Once this is called, the bitmap 143 | * will appear when text_draw_all() is called. 144 | */ 145 | int redraw_text(int text_id); 146 | 147 | /** 148 | * Draw all text objects to the canvas. 149 | * we support writing on two types of canvas: ARGB8888 (is_video = 0) and YUV420PackedPlanar (is_video = 1) 150 | * 151 | * returns: nonzero if the canvas content has been changed 152 | */ 153 | int text_draw_all(uint8_t *canvas, int canvas_width, int canvas_height, int stride, int is_video); 154 | 155 | /** 156 | * Clear the text. Once this is called, the bitmap will not be drawn 157 | * until text_set_text() is called. 158 | */ 159 | int text_clear(int text_id); 160 | 161 | /** 162 | * Calculates the top-left corner position for the text object on the canvas. 163 | */ 164 | int text_get_position(int text_id, int canvas_width, int canvas_height, int *x, int *y); 165 | 166 | /** 167 | * Select an appropriate font file and face index by a font name. 168 | */ 169 | int text_select_font_file(const char *name, char **font_file, int *face_index); 170 | 171 | /** 172 | * Destroys a text that is currently being displayed with old_text_id when new_text_id is drawn on screen. 173 | */ 174 | int text_destroy_on_appear(int old_text_id, int new_text_id); 175 | 176 | #endif // PICAM_TEXT_H 177 | 178 | #ifdef __cplusplus 179 | } 180 | #endif -------------------------------------------------------------------------------- /timestamp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(timestamp STATIC timestamp.c) 6 | target_link_libraries(timestamp text) 7 | 8 | install(TARGETS timestamp LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /timestamp/timestamp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "text/text.h" 7 | #include "timestamp.h" 8 | 9 | static const char *default_time_format = "%Y-%m-%d %H:%M:%S"; 10 | static const char *default_font_name = "Nimbus Mono L,monospace"; 11 | 12 | static int text_id = -1; 13 | static char time_format[128]; 14 | static time_t last_time_drawn; 15 | 16 | /** 17 | * Initializes the timestamp library with a font name. 18 | */ 19 | void timestamp_init_with_font_name(const char *font_name, int points, int dpi) { 20 | char *font_file; 21 | int face_index; 22 | 23 | if (font_name != NULL) { 24 | text_select_font_file(font_name, &font_file, &face_index); 25 | } else { 26 | text_select_font_file(default_font_name, &font_file, &face_index); 27 | } 28 | timestamp_init(font_file, face_index, points, dpi); 29 | free(font_file); 30 | } 31 | 32 | /** 33 | * Initializes the timestamp library with a font file and face index. 34 | */ 35 | void timestamp_init(const char *font_file, long face_index, int points, int dpi) { 36 | text_id = text_create( 37 | font_file, face_index, 38 | points, 39 | dpi 40 | ); 41 | timestamp_set_format(default_time_format); 42 | text_set_stroke_color(text_id, 0x000000); 43 | text_set_stroke_width(text_id, 1.0f); 44 | text_set_color(text_id, 0xffffff); 45 | text_set_layout(text_id, 46 | LAYOUT_ALIGN_BOTTOM | LAYOUT_ALIGN_RIGHT, // layout alignment for the box 47 | 5, // horizontal margin from the right edge 48 | 5); // vertical margin from the bottom edge 49 | text_set_align(text_id, TEXT_ALIGN_LEFT); // text alignment inside the box 50 | last_time_drawn = 0; 51 | } 52 | 53 | /** 54 | * Sets timestamp text format. 55 | */ 56 | void timestamp_set_format(const char *format) { 57 | strncpy(time_format, format, sizeof(time_format) - 1); 58 | time_format[sizeof(time_format) - 1] = '\0'; 59 | } 60 | 61 | /** 62 | * Sets text color. 63 | */ 64 | void timestamp_set_color(int color) { 65 | text_set_color(text_id, color); 66 | } 67 | 68 | /** 69 | * Sets text stroke color. 70 | */ 71 | void timestamp_set_stroke_color(uint32_t color) { 72 | text_set_stroke_color(text_id, color); 73 | } 74 | 75 | /** 76 | * Sets text stroke border width in points. 77 | */ 78 | void timestamp_set_stroke_width(float stroke_width) { 79 | text_set_stroke_width(text_id, stroke_width); 80 | } 81 | 82 | /** 83 | * Sets letter spacing in pixels. 84 | */ 85 | void timestamp_set_letter_spacing(int pixels) { 86 | text_set_letter_spacing(text_id, pixels); 87 | } 88 | 89 | /** 90 | * Sets multiplying factor for line spacing. 91 | * If this is set to 1, default line spacing is used. 92 | */ 93 | void timestamp_set_line_height_multiply(float multiply) { 94 | text_set_line_height_multiply(text_id, multiply); 95 | } 96 | 97 | /** 98 | * Sets the absolute position for the timestamp. 99 | */ 100 | void timestamp_set_position(int x, int y) { 101 | text_set_position(text_id, x, y); 102 | } 103 | 104 | /** 105 | * Sets the relative layout for the text in the screen. 106 | */ 107 | void timestamp_set_layout(LAYOUT_ALIGN layout_align, int horizontal_margin, int vertical_margin) { 108 | text_set_layout(text_id, layout_align, horizontal_margin, vertical_margin); 109 | } 110 | 111 | /** 112 | * Sets the text alignment inside a positioned box. 113 | */ 114 | void timestamp_set_align(TEXT_ALIGN text_align) { 115 | text_set_align(text_id, text_align); 116 | } 117 | 118 | /** 119 | * Assigns an absolute position for the timestamp based on 120 | * the relative layout constraints that is currently set. 121 | */ 122 | void timestamp_fix_position(int canvas_width, int canvas_height) { 123 | struct tm *timeinfo; 124 | char str[128]; 125 | 126 | time_t rawtime = 0; 127 | timeinfo = gmtime(&rawtime); 128 | strftime(str, sizeof(str)-1, time_format, timeinfo); 129 | str[sizeof(str)-1] = '\0'; 130 | 131 | text_set_text(text_id, str, strlen(str)); 132 | redraw_text(text_id); 133 | text_fix_position(text_id, canvas_width, canvas_height); 134 | text_clear(text_id); 135 | } 136 | 137 | /** 138 | * Call this function before calling text_draw_all(). 139 | */ 140 | void timestamp_update() { 141 | time_t rawtime; 142 | struct tm *timeinfo; 143 | char str[128]; 144 | 145 | time(&rawtime); 146 | 147 | if (rawtime > last_time_drawn) { 148 | timeinfo = localtime(&rawtime); 149 | strftime(str, sizeof(str)-1, time_format, timeinfo); 150 | str[sizeof(str)-1] = '\0'; 151 | 152 | text_set_text(text_id, str, strlen(str)); 153 | redraw_text(text_id); 154 | last_time_drawn = rawtime; 155 | } 156 | } 157 | 158 | /** 159 | * Destroys the resources used by timestamp library. 160 | */ 161 | void timestamp_shutdown() { 162 | text_destroy(text_id); 163 | } 164 | -------------------------------------------------------------------------------- /timestamp/timestamp.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | #ifndef PICAM_TIMESTAMP_H 6 | #define PICAM_TIMESTAMP_H 7 | 8 | #include 9 | #include "text/text.h" 10 | 11 | /** 12 | * Initializes the timestamp library with a font name. 13 | */ 14 | void timestamp_init_with_font_name(const char *font_name, int points, int dpi); 15 | 16 | /** 17 | * Initializes the timestamp library with a font file and face index. 18 | */ 19 | void timestamp_init(const char *font_file, long face_index, int points, int dpi); 20 | 21 | /** 22 | * Destroys the resources used by timestamp library. 23 | */ 24 | void timestamp_shutdown(); 25 | 26 | /** 27 | * Sets timestamp text format. 28 | */ 29 | void timestamp_set_format(const char *format); 30 | 31 | /** 32 | * Sets text color. 33 | */ 34 | void timestamp_set_color(int color); 35 | 36 | /** 37 | * Sets text stroke color. 38 | */ 39 | void timestamp_set_stroke_color(uint32_t color); 40 | 41 | /** 42 | * Sets text stroke border width in points. 43 | */ 44 | void timestamp_set_stroke_width(float stroke_width); 45 | 46 | /** 47 | * Sets letter spacing in pixels. 48 | */ 49 | void timestamp_set_letter_spacing(int pixels); 50 | 51 | /** 52 | * Sets multiplying factor for line spacing. 53 | * If this is set to 1, default line spacing is used. 54 | */ 55 | void timestamp_set_line_height_multiply(float line_height_multiply); 56 | 57 | /** 58 | * Sets the absolute position for the timestamp. 59 | */ 60 | void timestamp_set_position(int x, int y); 61 | 62 | /** 63 | * Sets the relative layout for the text in the screen. 64 | */ 65 | void timestamp_set_layout(LAYOUT_ALIGN layout_align, int horizontal_margin, int vertical_margin); 66 | 67 | /** 68 | * Sets the text alignment inside a positioned box. 69 | */ 70 | void timestamp_set_align(TEXT_ALIGN text_align); 71 | 72 | /** 73 | * Assigns an absolute position for the timestamp based on 74 | * the relative layout constraints that is currently set. 75 | */ 76 | void timestamp_fix_position(int canvas_width, int canvas_height); 77 | 78 | /** 79 | * Call this function before calling text_draw_all(). 80 | */ 81 | void timestamp_update(); 82 | 83 | #endif // PICAM_TIMESTAMP_H 84 | 85 | #ifdef __cplusplus 86 | } 87 | #endif -------------------------------------------------------------------------------- /video_encoder/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | include(GNUInstallDirs) 4 | 5 | add_library(video_encoder STATIC video_encoder.cpp) 6 | target_link_libraries(video_encoder text subtitle timestamp) 7 | 8 | install(TARGETS video_encoder LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) 9 | 10 | -------------------------------------------------------------------------------- /video_encoder/video_encoder.cpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. 4 | * 5 | * h264_encoder.cpp - h264 video encoder. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "video_encoder.hpp" 21 | #include "text/text.h" 22 | #include "subtitle/subtitle.h" 23 | #include "timestamp/timestamp.h" 24 | 25 | static int xioctl(int fd, unsigned long ctl, void *arg) 26 | { 27 | int ret, num_tries = 10; 28 | do 29 | { 30 | ret = ioctl(fd, ctl, arg); 31 | } while (ret == -1 && errno == EINTR && num_tries-- > 0); 32 | return ret; 33 | } 34 | 35 | static int get_v4l2_colorspace(std::optional const &cs) 36 | { 37 | if (cs == libcamera::ColorSpace::Rec709) 38 | return V4L2_COLORSPACE_REC709; 39 | else if (cs == libcamera::ColorSpace::Smpte170m) 40 | return V4L2_COLORSPACE_SMPTE170M; 41 | 42 | std::cerr << "H264: surprising colour space: " << libcamera::ColorSpace::toString(cs) << std::endl; 43 | return V4L2_COLORSPACE_SMPTE170M; 44 | } 45 | 46 | VideoEncoder::VideoEncoder(PicamOption const *options, StreamInfo const &info) 47 | : abortPoll_(false), abortOutput_(false) 48 | { 49 | // First open the encoder device. Maybe we should double-check its "caps". 50 | 51 | const char device_name[] = "/dev/video11"; 52 | fd_ = open(device_name, O_RDWR, 0); 53 | if (fd_ < 0) { 54 | throw std::runtime_error("failed to open V4L2 H264 encoder"); 55 | } 56 | log_debug("Opened VideoEncoder on %s as fd %d\n", device_name, fd_); 57 | 58 | // Apply any options-> 59 | 60 | v4l2_control ctrl = {}; 61 | ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE; 62 | ctrl.value = options->video_bitrate; 63 | if (xioctl(fd_, VIDIOC_S_CTRL, &ctrl) < 0) { 64 | throw std::runtime_error("failed to set bitrate"); 65 | } 66 | 67 | ctrl.id = V4L2_CID_MPEG_VIDEO_H264_PROFILE; 68 | v4l2_mpeg_video_h264_profile profile = V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE; 69 | for (unsigned int i = 0; i < sizeof(video_avc_profile_options) / sizeof(video_avc_profile_option); i++) { 70 | if (strcmp(video_avc_profile_options[i].name, options->video_avc_profile) == 0) { 71 | profile = video_avc_profile_options[i].profile; 72 | break; 73 | } 74 | } 75 | ctrl.value = profile; 76 | if (xioctl(fd_, VIDIOC_S_CTRL, &ctrl) < 0) { 77 | throw std::runtime_error("failed to set profile"); 78 | } 79 | 80 | ctrl.id = V4L2_CID_MPEG_VIDEO_H264_LEVEL; 81 | v4l2_mpeg_video_h264_level level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1; 82 | for (unsigned int i = 0; i < sizeof(video_avc_level_options) / sizeof(video_avc_level_option); i++) { 83 | if (strcmp(video_avc_level_options[i].name, options->video_avc_level) == 0) { 84 | level = video_avc_level_options[i].level; 85 | break; 86 | } 87 | } 88 | ctrl.value = level; 89 | if (xioctl(fd_, VIDIOC_S_CTRL, &ctrl) < 0) { 90 | throw std::runtime_error("failed to set level"); 91 | } 92 | 93 | this->setGopSize(options->video_gop_size); 94 | 95 | // if (options->inline_headers) 96 | // { 97 | // ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER; 98 | // ctrl.value = 1; 99 | // if (xioctl(fd_, VIDIOC_S_CTRL, &ctrl) < 0) 100 | // throw std::runtime_error("failed to set inline headers"); 101 | // } 102 | 103 | // Set the output and capture formats. We know exactly what they will be. 104 | 105 | v4l2_format fmt = {}; 106 | fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 107 | fmt.fmt.pix_mp.width = info.width; 108 | fmt.fmt.pix_mp.height = info.height; 109 | // We assume YUV420 here, but it would be nice if we could do something 110 | // like info.pixel_format.toV4L2Fourcc(); 111 | fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420; 112 | fmt.fmt.pix_mp.plane_fmt[0].bytesperline = info.stride; 113 | fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; 114 | fmt.fmt.pix_mp.colorspace = get_v4l2_colorspace(info.colour_space); 115 | fmt.fmt.pix_mp.num_planes = 1; 116 | if (xioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) 117 | throw std::runtime_error("failed to set output format"); 118 | 119 | fmt = {}; 120 | fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 121 | fmt.fmt.pix_mp.width = options->video_width; 122 | fmt.fmt.pix_mp.height = options->video_height; 123 | fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264; 124 | fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; 125 | fmt.fmt.pix_mp.colorspace = V4L2_COLORSPACE_DEFAULT; 126 | fmt.fmt.pix_mp.num_planes = 1; 127 | fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0; 128 | fmt.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10; 129 | if (xioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) 130 | throw std::runtime_error("failed to set capture format"); 131 | 132 | struct v4l2_streamparm parm = {}; 133 | parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 134 | parm.parm.output.timeperframe.numerator = 1000 / options->video_fps; 135 | parm.parm.output.timeperframe.denominator = 1000; 136 | if (xioctl(fd_, VIDIOC_S_PARM, &parm) < 0) 137 | throw std::runtime_error("failed to set streamparm"); 138 | 139 | // Request that the necessary buffers are allocated. The output queue 140 | // (input to the encoder) shares buffers from our caller, these must be 141 | // DMABUFs. Buffers for the encoded bitstream must be allocated and 142 | // m-mapped. 143 | 144 | v4l2_requestbuffers reqbufs = {}; 145 | reqbufs.count = NUM_OUTPUT_BUFFERS; 146 | reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 147 | reqbufs.memory = V4L2_MEMORY_DMABUF; 148 | if (xioctl(fd_, VIDIOC_REQBUFS, &reqbufs) < 0) 149 | throw std::runtime_error("request for output buffers failed"); 150 | log_debug("Got %u output buffers\n", reqbufs.count); 151 | 152 | // We have to maintain a list of the buffers we can use when our caller gives 153 | // us another frame to encode. 154 | for (unsigned int i = 0; i < reqbufs.count; i++) 155 | input_buffers_available_.push(i); 156 | 157 | reqbufs = {}; 158 | reqbufs.count = NUM_CAPTURE_BUFFERS; 159 | reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 160 | reqbufs.memory = V4L2_MEMORY_MMAP; 161 | if (xioctl(fd_, VIDIOC_REQBUFS, &reqbufs) < 0) 162 | throw std::runtime_error("request for capture buffers failed"); 163 | log_debug("Got %u capture buffers\n", reqbufs.count); 164 | num_capture_buffers_ = reqbufs.count; 165 | 166 | for (unsigned int i = 0; i < reqbufs.count; i++) 167 | { 168 | v4l2_plane planes[VIDEO_MAX_PLANES]; 169 | v4l2_buffer buffer = {}; 170 | buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 171 | buffer.memory = V4L2_MEMORY_MMAP; 172 | buffer.index = i; 173 | buffer.length = 1; 174 | buffer.m.planes = planes; 175 | if (xioctl(fd_, VIDIOC_QUERYBUF, &buffer) < 0) 176 | throw std::runtime_error("failed to capture query buffer " + std::to_string(i)); 177 | buffers_[i].mem = mmap(0, buffer.m.planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 178 | buffer.m.planes[0].m.mem_offset); 179 | if (buffers_[i].mem == MAP_FAILED) 180 | throw std::runtime_error("failed to mmap capture buffer " + std::to_string(i)); 181 | buffers_[i].size = buffer.m.planes[0].length; 182 | // Whilst we're going through all the capture buffers, we may as well queue 183 | // them ready for the encoder to write into. 184 | if (xioctl(fd_, VIDIOC_QBUF, &buffer) < 0) 185 | throw std::runtime_error("failed to queue capture buffer " + std::to_string(i)); 186 | } 187 | 188 | // Enable streaming and we're done. 189 | 190 | v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 191 | if (xioctl(fd_, VIDIOC_STREAMON, &type) < 0) { 192 | if (strncmp(options->video_avc_level, "4", 1) != 0) { 193 | throw std::runtime_error("Failed to start output streaming. Note that --avclevel below 4.0 does not work with higher resolutions."); 194 | } 195 | throw std::runtime_error("Failed to start output streaming."); 196 | } 197 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 198 | if (xioctl(fd_, VIDIOC_STREAMON, &type) < 0) 199 | throw std::runtime_error("failed to start capture streaming"); 200 | log_debug("Codec streaming started\n"); 201 | 202 | output_thread_ = std::thread(&VideoEncoder::outputThread, this); 203 | poll_thread_ = std::thread(&VideoEncoder::pollThread, this); 204 | } 205 | 206 | VideoEncoder::~VideoEncoder() 207 | { 208 | abortPoll_ = true; 209 | poll_thread_.join(); 210 | abortOutput_ = true; 211 | output_thread_.join(); 212 | 213 | // Turn off streaming on both the output and capture queues, and "free" the 214 | // buffers that we requested. The capture ones need to be "munmapped" first. 215 | 216 | v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 217 | if (xioctl(fd_, VIDIOC_STREAMOFF, &type) < 0) 218 | std::cerr << "Failed to stop output streaming" << std::endl; 219 | type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 220 | if (xioctl(fd_, VIDIOC_STREAMOFF, &type) < 0) 221 | std::cerr << "Failed to stop capture streaming" << std::endl; 222 | 223 | v4l2_requestbuffers reqbufs = {}; 224 | reqbufs.count = 0; 225 | reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 226 | reqbufs.memory = V4L2_MEMORY_DMABUF; 227 | if (xioctl(fd_, VIDIOC_REQBUFS, &reqbufs) < 0) 228 | std::cerr << "Request to free output buffers failed" << std::endl; 229 | 230 | for (int i = 0; i < num_capture_buffers_; i++) 231 | if (munmap(buffers_[i].mem, buffers_[i].size) < 0) 232 | std::cerr << "Failed to unmap buffer" << std::endl; 233 | reqbufs = {}; 234 | reqbufs.count = 0; 235 | reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 236 | reqbufs.memory = V4L2_MEMORY_MMAP; 237 | if (xioctl(fd_, VIDIOC_REQBUFS, &reqbufs) < 0) 238 | std::cerr << "Request to free capture buffers failed" << std::endl; 239 | 240 | close(fd_); 241 | log_debug("VideoEncoder closed\n"); 242 | } 243 | 244 | void VideoEncoder::setGopSize(int gop_size) 245 | { 246 | v4l2_control ctrl = {}; 247 | // Period between I-frames in the open GOP for H264. 248 | ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD; 249 | ctrl.value = gop_size; 250 | if (xioctl(fd_, VIDIOC_S_CTRL, &ctrl) < 0) { 251 | throw std::runtime_error("failed to set intra period"); 252 | } 253 | } 254 | 255 | void VideoEncoder::EncodeBuffer(int fd, size_t size, void *mem, StreamInfo const &info, int64_t timestamp_us) 256 | { 257 | // mem is a YUV frame 258 | 259 | int index; 260 | { 261 | // We need to find an available output buffer (input to the codec) to 262 | // "wrap" the DMABUF. 263 | std::lock_guard lock(input_buffers_available_mutex_); 264 | if (input_buffers_available_.empty()) 265 | throw std::runtime_error("no buffers available to queue codec input"); 266 | index = input_buffers_available_.front(); 267 | input_buffers_available_.pop(); 268 | } 269 | v4l2_buffer buf = {}; 270 | v4l2_plane planes[VIDEO_MAX_PLANES] = {}; 271 | buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 272 | buf.index = index; 273 | buf.field = V4L2_FIELD_NONE; 274 | buf.memory = V4L2_MEMORY_DMABUF; 275 | buf.length = 1; 276 | buf.timestamp.tv_sec = timestamp_us / 1000000; 277 | buf.timestamp.tv_usec = timestamp_us % 1000000; 278 | buf.m.planes = planes; 279 | buf.m.planes[0].m.fd = fd; 280 | buf.m.planes[0].bytesused = size; 281 | buf.m.planes[0].length = size; 282 | if (xioctl(fd_, VIDIOC_QBUF, &buf) < 0) 283 | throw std::runtime_error("failed to queue input to codec"); 284 | } 285 | 286 | void VideoEncoder::pollThread() 287 | { 288 | while (true) 289 | { 290 | pollfd p = { fd_, POLLIN, 0 }; 291 | int ret = poll(&p, 1, 200); 292 | { 293 | std::lock_guard lock(input_buffers_available_mutex_); 294 | if (abortPoll_ && input_buffers_available_.size() == NUM_OUTPUT_BUFFERS) 295 | break; 296 | } 297 | if (ret == -1) 298 | { 299 | if (errno == EINTR) 300 | continue; 301 | throw std::runtime_error("unexpected errno " + std::to_string(errno) + " from poll"); 302 | } 303 | if (p.revents & POLLIN) // There is data to read 304 | { 305 | v4l2_buffer buf = {}; 306 | v4l2_plane planes[VIDEO_MAX_PLANES] = {}; 307 | // v4l2_plane: 308 | // bytesused: The number of bytes occupied by data in the plane (its payload). 309 | // length: Size in bytes of the plane (not its payload). 310 | // m: union 311 | // mem_offset: When the memory type in the containing struct v4l2_buffer is V4L2_MEMORY_MMAP, this is the value that should be passed to mmap(), similar to the offset field in struct v4l2_buffer. 312 | // userptr: When the memory type in the containing struct v4l2_buffer is V4L2_MEMORY_USERPTR, this is a userspace pointer to the memory allocated for this plane by an application. 313 | // fd: When the memory type in the containing struct v4l2_buffer is V4L2_MEMORY_DMABUF, this is a file descriptor associated with a DMABUF buffer, similar to the fd field in struct v4l2_buffer. 314 | // data_offset: Offset in bytes to video data in the plane. 315 | buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; 316 | buf.memory = V4L2_MEMORY_DMABUF; 317 | buf.length = 1; 318 | buf.m.planes = planes; 319 | int ret = xioctl(fd_, VIDIOC_DQBUF, &buf); 320 | if (ret == 0) 321 | { 322 | // Return this to the caller, first noting that this buffer, identified 323 | // by its index, is available for queueing up another frame. 324 | { 325 | std::lock_guard lock(input_buffers_available_mutex_); 326 | input_buffers_available_.push(buf.index); 327 | } 328 | input_done_callback_(nullptr); 329 | } 330 | 331 | buf = {}; 332 | memset(planes, 0, sizeof(planes)); 333 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 334 | buf.memory = V4L2_MEMORY_MMAP; 335 | buf.length = 1; 336 | buf.m.planes = planes; 337 | ret = xioctl(fd_, VIDIOC_DQBUF, &buf); 338 | if (ret == 0) 339 | { 340 | // We push this encoded buffer to another thread so that our 341 | // application can take its time with the data without blocking the 342 | // encode process. 343 | int64_t timestamp_us = (buf.timestamp.tv_sec * (int64_t)1000000) + buf.timestamp.tv_usec; 344 | OutputItem item = { buffers_[buf.index].mem, // mem 345 | buf.m.planes[0].bytesused, // bytes_used 346 | buf.m.planes[0].length, // length 347 | buf.index, // index 348 | !!(buf.flags & V4L2_BUF_FLAG_KEYFRAME), // keyframe 349 | timestamp_us }; // timestamp_us 350 | std::lock_guard lock(output_mutex_); 351 | output_queue_.push(item); 352 | output_cond_var_.notify_one(); 353 | } 354 | } 355 | } 356 | } 357 | 358 | void VideoEncoder::outputThread() 359 | { 360 | OutputItem item; 361 | while (true) 362 | { 363 | { 364 | std::unique_lock lock(output_mutex_); 365 | while (true) 366 | { 367 | using namespace std::chrono_literals; 368 | // Must check the abort first, to allow items in the output 369 | // queue to have a callback. 370 | if (abortOutput_ && output_queue_.empty()) 371 | return; 372 | 373 | if (!output_queue_.empty()) 374 | { 375 | item = output_queue_.front(); 376 | output_queue_.pop(); 377 | break; 378 | } 379 | else 380 | output_cond_var_.wait_for(lock, 200ms); 381 | } 382 | } 383 | 384 | if (output_ready_callback_) { 385 | output_ready_callback_(item.mem, item.bytes_used, item.timestamp_us, item.keyframe); 386 | } 387 | v4l2_buffer buf = {}; 388 | v4l2_plane planes[VIDEO_MAX_PLANES] = {}; 389 | buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; 390 | buf.memory = V4L2_MEMORY_MMAP; 391 | buf.index = item.index; 392 | buf.length = 1; 393 | buf.m.planes = planes; 394 | buf.m.planes[0].bytesused = 0; 395 | buf.m.planes[0].length = item.length; 396 | if (xioctl(fd_, VIDIOC_QBUF, &buf) < 0) 397 | throw std::runtime_error("failed to re-queue encoded buffer"); 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /video_encoder/video_encoder.hpp: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-2-Clause */ 2 | /* 3 | * Based on h264_encoder.hpp - Copyright (C) 2020, Raspberry Pi (Trading) Ltd. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "picam_option/picam_option.hpp" 15 | #include "core/stream_info.hpp" 16 | 17 | // >>> encoder.hpp 18 | typedef std::function InputDoneCallback; 19 | typedef std::function OutputReadyCallback; 20 | // <<< encoder.hpp 21 | 22 | class VideoEncoder 23 | { 24 | public: 25 | VideoEncoder(PicamOption const *options, StreamInfo const &info); 26 | ~VideoEncoder(); 27 | void setGopSize(int gop_size); 28 | 29 | // >>> encoder.hpp 30 | // This is where the application sets the callback it gets whenever the encoder 31 | // has finished with an input buffer, so the application can re-use it. 32 | void SetInputDoneCallback(InputDoneCallback callback) { input_done_callback_ = callback; } 33 | // This callback is how the application is told that an encoded buffer is 34 | // available. The application may not hang on to the memory once it returns 35 | // (but the callback is already running in its own thread). 36 | void SetOutputReadyCallback(OutputReadyCallback callback) { output_ready_callback_ = callback; } 37 | // <<< encoder.hpp 38 | 39 | // >>> h264_encoder.hpp 40 | // Encode the given DMABUF. 41 | // Encode the given buffer. The buffer is specified both by an fd and size 42 | // describing a DMABUF, and by a mmapped userland pointer. 43 | void EncodeBuffer(int fd, size_t size, void *mem, StreamInfo const &info, int64_t timestamp_us); 44 | // <<< h264_encoder.hpp 45 | 46 | protected: 47 | // >>> encoder.hpp 48 | InputDoneCallback input_done_callback_; 49 | OutputReadyCallback output_ready_callback_; 50 | PicamOption const *options_; 51 | // <<< encoder.hpp 52 | 53 | private: 54 | // >>> h264_encoder.hpp 55 | // We want at least as many output buffers as there are in the camera queue 56 | // (we always want to be able to queue them when they arrive). Make loads 57 | // of capture buffers, as this is our buffering mechanism in case of delays 58 | // dealing with the output bitstream. 59 | static constexpr int NUM_OUTPUT_BUFFERS = 6; 60 | static constexpr int NUM_CAPTURE_BUFFERS = 12; 61 | 62 | // This thread just sits waiting for the encoder to finish stuff. It will either: 63 | // * receive "output" buffers (codec inputs), which we must return to the caller 64 | // * receive encoded buffers, which we pass to the application. 65 | void pollThread(); 66 | 67 | // Handle the output buffers in another thread so as not to block the encoder. The 68 | // application can take its time, after which we return this buffer to the encoder for 69 | // re-use. 70 | void outputThread(); 71 | 72 | bool abortPoll_; 73 | bool abortOutput_; 74 | int fd_; 75 | struct BufferDescription 76 | { 77 | void *mem; 78 | size_t size; 79 | }; 80 | BufferDescription buffers_[NUM_CAPTURE_BUFFERS]; 81 | int num_capture_buffers_; 82 | std::thread poll_thread_; 83 | std::mutex input_buffers_available_mutex_; 84 | std::queue input_buffers_available_; 85 | struct OutputItem 86 | { 87 | void *mem; 88 | size_t bytes_used; 89 | size_t length; 90 | unsigned int index; 91 | bool keyframe; 92 | int64_t timestamp_us; 93 | }; 94 | std::queue output_queue_; 95 | std::mutex output_mutex_; 96 | std::condition_variable output_cond_var_; 97 | std::thread output_thread_; 98 | // <<< h264_encoder.hpp 99 | }; 100 | --------------------------------------------------------------------------------