├── .nojekyll ├── tests ├── .gitignore ├── types │ ├── request_uri.c │ ├── reason_phrase.c │ ├── message_body.c │ ├── method.c │ ├── header.c │ ├── rtsp_version.c │ ├── status_code.c │ ├── request_line.c │ ├── response_line.c │ ├── sdp.c │ ├── request.c │ ├── response.c │ ├── test_util.h │ └── header_map.c ├── io_vec.c ├── main.c ├── CMakeLists.txt ├── nal │ ├── h264.c │ └── h265.c ├── transport.c ├── writer.c ├── context.c └── util.c ├── examples ├── .gitignore ├── media │ ├── .gitignore │ ├── audio.g711a │ └── video.h264 ├── cmake │ └── modules │ │ └── FindLibEvent.cmake └── CMakeLists.txt ├── .mailmap ├── scripts ├── docs.sh ├── open-docs.sh ├── build.sh ├── test.sh ├── check-fmt.sh ├── fmt.sh ├── test-server-ffmpeg.sh └── test-server.sh ├── media └── example-server-demo.png ├── .gitmodules ├── src ├── io_vec.c ├── writer.c ├── macros.h ├── types │ ├── method.c │ ├── request_uri.c │ ├── reason_phrase.c │ ├── message_body.c │ ├── sdp.c │ ├── status_code.c │ ├── header.c │ ├── response_line.c │ ├── request_line.c │ ├── rtsp_version.c │ ├── parsing.h │ ├── rtp.c │ ├── header_map.c │ ├── error.c │ ├── request.c │ ├── response.c │ └── parsing.c ├── writer │ ├── fd.c │ ├── string.c │ └── file.c ├── controller.c ├── nal │ ├── h264.c │ └── h265.c ├── transport │ ├── tcp.c │ └── udp.c ├── rtp_transport.c ├── context.c ├── util.c ├── nal_transport.c └── nal.c ├── include ├── smolrtsp │ ├── priv │ │ └── compiler_attrs.h │ ├── io_vec.h │ ├── types │ │ ├── request_uri.h │ │ ├── reason_phrase.h │ │ ├── message_body.h │ │ ├── rtsp_version.h │ │ ├── request_line.h │ │ ├── response_line.h │ │ ├── method.h │ │ ├── request.h │ │ ├── response.h │ │ ├── header_map.h │ │ ├── rtp.h │ │ └── error.h │ ├── droppable.h │ ├── option.h │ ├── rtp_transport.h │ ├── nal_transport.h │ ├── context.h │ ├── transport.h │ ├── util.h │ ├── nal.h │ └── writer.h └── smolrtsp.h ├── .clang-format ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── .github └── workflows │ └── c-cpp.yml ├── README.md └── CMakeLists.txt /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | hirrolot 2 | -------------------------------------------------------------------------------- /scripts/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | doxygen 4 | -------------------------------------------------------------------------------- /examples/media/.gitignore: -------------------------------------------------------------------------------- 1 | audio.g711a.h 2 | video.h264.h 3 | -------------------------------------------------------------------------------- /scripts/open-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | xdg-open docs/index.html 4 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir build -p 4 | cd build 5 | cmake .. 6 | cmake --build . 7 | -------------------------------------------------------------------------------- /examples/media/audio.g711a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/smolrtsp/master/examples/media/audio.g711a -------------------------------------------------------------------------------- /examples/media/video.h264: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/smolrtsp/master/examples/media/video.h264 -------------------------------------------------------------------------------- /media/example-server-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenIPC/smolrtsp/master/media/example-server-demo.png -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir tests/build -p 4 | cd tests/build 5 | cmake .. 6 | cmake --build . 7 | ./tests 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "run-clang-format"] 2 | path = run-clang-format 3 | url = https://github.com/Sarcasm/run-clang-format.git 4 | -------------------------------------------------------------------------------- /scripts/check-fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./run-clang-format/run-clang-format.py \ 4 | --exclude tests/build \ 5 | -r include src tests 6 | -------------------------------------------------------------------------------- /scripts/fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find include src tests \ 4 | \( -path examples/build -o -path tests/build \) -prune -false -o \ 5 | \( -iname "*.h" \) -or \( -iname "*.c" \) | xargs clang-format -i 6 | -------------------------------------------------------------------------------- /scripts/test-server-ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ffmpeg -y -rtsp_transport $1 -i rtsp://localhost -vcodec copy test_$1.h264 -acodec copy -f mulaw test_$1.g711a 6 | cmp test_$1.h264 examples/media/video.h264 7 | cmp test_$1.g711a examples/media/audio.g711a 8 | 9 | echo "Server '$1' is ok." 10 | -------------------------------------------------------------------------------- /scripts/test-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | mkdir examples/build -p 6 | cd examples/build 7 | cmake .. 8 | cmake --build . 9 | ./server & 10 | server_pid=$! 11 | 12 | sleep 2s 13 | 14 | cd ../.. 15 | 16 | bash scripts/test-server-ffmpeg.sh tcp & 17 | tcp_pid=$! 18 | bash scripts/test-server-ffmpeg.sh udp & 19 | udp_pid=$! 20 | 21 | wait $tcp_pid 22 | wait $udp_pid 23 | kill $server_pid 24 | -------------------------------------------------------------------------------- /src/io_vec.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | size_t SmolRTSP_IoVecSlice_len(SmolRTSP_IoVecSlice self) { 4 | size_t result = 0; 5 | for (size_t i = 0; i < self.len; i++) { 6 | result += self.ptr[i].iov_len; 7 | } 8 | 9 | return result; 10 | } 11 | 12 | struct iovec smolrtsp_slice_to_iovec(U8Slice99 slice) { 13 | return (struct iovec){.iov_base = slice.ptr, .iov_len = slice.len}; 14 | } 15 | -------------------------------------------------------------------------------- /tests/types/request_uri.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_RequestUri) 7 | 8 | TEST parse_request_uri(void) { 9 | TEST_PARSE( 10 | "http://example.com ", CharSlice99_from_str("http://example.com")); 11 | 12 | PASS(); 13 | } 14 | 15 | SUITE(types_request_uri) { 16 | RUN_TEST(parse_request_uri); 17 | } 18 | -------------------------------------------------------------------------------- /tests/types/reason_phrase.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_ReasonPhrase) 7 | 8 | TEST parse_reason_phrase(void) { 9 | TEST_PARSE( 10 | "Moved Temporarily\r\n", CharSlice99_from_str("Moved Temporarily")); 11 | 12 | PASS(); 13 | } 14 | 15 | SUITE(types_reason_phrase) { 16 | RUN_TEST(parse_reason_phrase); 17 | } 18 | -------------------------------------------------------------------------------- /include/smolrtsp/priv/compiler_attrs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __GNUC__ 4 | #define SMOLRTSP_PRIV_GNUC_ATTR(attr) __attribute__((attr)) 5 | #else 6 | #define SMOLRTSP_PRIV_GNUC_ATTR(_attr) 7 | #endif 8 | 9 | #if defined(__GNUC__) && !defined(__clang__) 10 | #define SMOLRTSP_PRIV_GCC_ATTR(attr) __attribute__((attr)) 11 | #else 12 | #define SMOLRTSP_PRIV_GCC_ATTR(_attr) 13 | #endif 14 | 15 | #define SMOLRTSP_PRIV_MUST_USE SMOLRTSP_PRIV_GNUC_ATTR(warn_unused_result) 16 | -------------------------------------------------------------------------------- /src/writer.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "macros.h" 4 | 5 | #include 6 | 7 | ssize_t smolrtsp_write_slices( 8 | SmolRTSP_Writer w, size_t len, 9 | const CharSlice99 data[restrict static len]) { 10 | assert(w.self && w.vptr); 11 | 12 | ssize_t result = 0, ret = 0; 13 | 14 | for (size_t i = 0; i < len; i++) { 15 | ret = VCALL(w, write, data[i]); 16 | CHK_WRITE_ERR(result, ret); 17 | } 18 | 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: LLVM 3 | 4 | IndentWidth: 4 5 | ContinuationIndentWidth: 4 6 | ColumnLimit: 80 7 | 8 | AlwaysBreakBeforeMultilineStrings: true 9 | AllowShortFunctionsOnASingleLine: Empty 10 | AlignConsecutiveMacros: true 11 | AlignAfterOpenBracket: AlwaysBreak 12 | 13 | StatementMacros: 14 | [ 15 | "vfunc99", 16 | "vfuncDefault99", 17 | "DEF_TEST_PARSE", 18 | "NAL_HEADER_DERIVE_GETTER", 19 | "NAL_HEADER_DERIVE_PREDICATE", 20 | "NAL_HEADER_TEST_GETTER", 21 | ] 22 | -------------------------------------------------------------------------------- /include/smolrtsp/io_vec.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Vectored I/O support. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | 17 | /** 18 | * A slice of elements of type `struct iovec`. 19 | */ 20 | SLICE99_DEF_TYPED(SmolRTSP_IoVecSlice, struct iovec); 21 | 22 | /** 23 | * Computes the total length of @p self. 24 | */ 25 | size_t SmolRTSP_IoVecSlice_len(SmolRTSP_IoVecSlice self) SMOLRTSP_PRIV_MUST_USE; 26 | 27 | /** 28 | * Converts an octet slice @p slice to `struct iovec`. 29 | */ 30 | struct iovec smolrtsp_slice_to_iovec(U8Slice99 slice) SMOLRTSP_PRIV_MUST_USE; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # CMake build files 55 | build/ 56 | 57 | # Doxygen 58 | docs/ 59 | -------------------------------------------------------------------------------- /src/macros.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define CHK_WRITE_ERR(result, call) \ 6 | do { \ 7 | ssize_t CHK_WRITE_ERR_ret = call; \ 8 | if (CHK_WRITE_ERR_ret < 0) { \ 9 | return result; \ 10 | } else { \ 11 | result += CHK_WRITE_ERR_ret; \ 12 | } \ 13 | } while (0) 14 | -------------------------------------------------------------------------------- /src/types/method.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | 7 | SmolRTSP_ParseResult 8 | SmolRTSP_Method_parse(SmolRTSP_Method *restrict self, CharSlice99 input) { 9 | assert(self); 10 | 11 | const CharSlice99 backup = input; 12 | 13 | MATCH(smolrtsp_match_whitespaces(input)); 14 | CharSlice99 method = input; 15 | MATCH(smolrtsp_match_ident(input)); 16 | method = CharSlice99_from_ptrdiff(method.ptr, input.ptr); 17 | 18 | *self = method; 19 | 20 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 21 | } 22 | 23 | bool SmolRTSP_Method_eq( 24 | const SmolRTSP_Method *restrict lhs, const SmolRTSP_Method *restrict rhs) { 25 | assert(lhs); 26 | assert(rhs); 27 | 28 | return CharSlice99_primitive_eq(*lhs, *rhs); 29 | } 30 | -------------------------------------------------------------------------------- /src/types/request_uri.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | 7 | SmolRTSP_ParseResult SmolRTSP_RequestUri_parse( 8 | SmolRTSP_RequestUri *restrict self, CharSlice99 input) { 9 | assert(self); 10 | 11 | const CharSlice99 backup = input; 12 | 13 | MATCH(smolrtsp_match_whitespaces(input)); 14 | CharSlice99 uri = input; 15 | MATCH(smolrtsp_match_non_whitespaces(input)); 16 | uri = CharSlice99_from_ptrdiff(uri.ptr, input.ptr); 17 | 18 | *self = uri; 19 | 20 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 21 | } 22 | 23 | bool SmolRTSP_RequestUri_eq( 24 | const SmolRTSP_RequestUri *restrict lhs, 25 | const SmolRTSP_RequestUri *restrict rhs) { 26 | assert(lhs); 27 | assert(rhs); 28 | 29 | return CharSlice99_primitive_eq(*lhs, *rhs); 30 | } 31 | -------------------------------------------------------------------------------- /tests/io_vec.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST io_vec_len(void) { 6 | struct iovec bufs[] = { 7 | {.iov_base = NULL, .iov_len = 5}, 8 | {.iov_base = NULL, .iov_len = 1}, 9 | {.iov_base = NULL, .iov_len = 0}, 10 | }; 11 | 12 | ASSERT_EQ( 13 | SmolRTSP_IoVecSlice_len( 14 | (SmolRTSP_IoVecSlice)Slice99_typed_from_array(bufs)), 15 | 5 + 1 + 0); 16 | 17 | PASS(); 18 | } 19 | 20 | TEST slice_to_iovec(void) { 21 | uint8_t data[] = {1, 2, 3}; 22 | const struct iovec bufs = 23 | smolrtsp_slice_to_iovec((U8Slice99)Slice99_typed_from_array(data)); 24 | 25 | ASSERT_EQ(bufs.iov_base, data); 26 | ASSERT_EQ(bufs.iov_len, sizeof data); 27 | 28 | PASS(); 29 | } 30 | 31 | SUITE(io_vec) { 32 | RUN_TEST(io_vec_len); 33 | RUN_TEST(slice_to_iovec); 34 | } 35 | -------------------------------------------------------------------------------- /include/smolrtsp/types/request_uri.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP request URI. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | /** 16 | * An RTSP request URI. 17 | */ 18 | typedef CharSlice99 SmolRTSP_RequestUri; 19 | 20 | /** 21 | * Parses @p data to @p self. 22 | * 23 | * @pre `self != NULL` 24 | */ 25 | SmolRTSP_ParseResult SmolRTSP_RequestUri_parse( 26 | SmolRTSP_RequestUri *restrict self, 27 | CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 28 | 29 | /** 30 | * Tests @p lhs and @p rhs for equality. 31 | * 32 | * @pre `lhs != NULL` 33 | * @pre `rhs != NULL` 34 | */ 35 | bool SmolRTSP_RequestUri_eq( 36 | const SmolRTSP_RequestUri *restrict lhs, 37 | const SmolRTSP_RequestUri *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 38 | -------------------------------------------------------------------------------- /include/smolrtsp/types/reason_phrase.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP reason phrase. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | /** 16 | * An RTSP reason phrase. 17 | */ 18 | typedef CharSlice99 SmolRTSP_ReasonPhrase; 19 | 20 | /** 21 | * Parses @p data to @p self. 22 | * 23 | * @pre `self != NULL` 24 | */ 25 | SmolRTSP_ParseResult SmolRTSP_ReasonPhrase_parse( 26 | SmolRTSP_ReasonPhrase *restrict self, 27 | CharSlice99 data) SMOLRTSP_PRIV_MUST_USE; 28 | 29 | /** 30 | * Tests @p lhs and @p rhs for equality. 31 | * 32 | * @pre `lhs != NULL` 33 | * @pre `rhs != NULL` 34 | */ 35 | bool SmolRTSP_ReasonPhrase_eq( 36 | const SmolRTSP_ReasonPhrase *restrict lhs, 37 | const SmolRTSP_ReasonPhrase *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 38 | -------------------------------------------------------------------------------- /src/types/reason_phrase.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | 7 | SmolRTSP_ParseResult SmolRTSP_ReasonPhrase_parse( 8 | SmolRTSP_ReasonPhrase *restrict self, CharSlice99 input) { 9 | assert(self); 10 | 11 | const CharSlice99 backup = input; 12 | 13 | MATCH(smolrtsp_match_whitespaces(input)); 14 | CharSlice99 phrase = input; 15 | MATCH(smolrtsp_match_until_crlf(input)); 16 | phrase = CharSlice99_from_ptrdiff(phrase.ptr, input.ptr - strlen("\r\n")); 17 | 18 | *self = phrase; 19 | 20 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 21 | } 22 | 23 | bool SmolRTSP_ReasonPhrase_eq( 24 | const SmolRTSP_ReasonPhrase *restrict lhs, 25 | const SmolRTSP_ReasonPhrase *restrict rhs) { 26 | assert(lhs); 27 | assert(rhs); 28 | 29 | return CharSlice99_primitive_eq(*lhs, *rhs); 30 | } 31 | -------------------------------------------------------------------------------- /include/smolrtsp/droppable.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Droppable types support. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | /** 11 | * Types that can be dropped (deallocated). 12 | * 13 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 14 | * usage. 15 | */ 16 | #define SmolRTSP_Droppable_IFACE \ 17 | \ 18 | /* \ 19 | * Deallocates itself so that it can no longer be used. \ 20 | */ \ 21 | vfunc99(void, drop, VSelf99) 22 | 23 | /** 24 | * Defines the `SmolRTSP_Droppable` interface. 25 | * 26 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 27 | * usage. 28 | */ 29 | interface99(SmolRTSP_Droppable); 30 | -------------------------------------------------------------------------------- /include/smolrtsp/option.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Optional values. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | /** 12 | * Defines a typed optional value. 13 | * 14 | * - `SMOLRTSP_DEF_OPTION(T)` expands to `datatype99(T##Option, (T##_Some, T), 15 | * (T##_None))`. 16 | * - `SMOLRTSP_DEF_OPTION(name, T)` expands to `datatype99(name##Option, 17 | * (name##_Some, T), (name##_None))`. 18 | * 19 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 20 | */ 21 | #define SMOLRTSP_DEF_OPTION(...) \ 22 | ML99_OVERLOAD(SMOLRTSP_PRIV_DEF_OPTION_, __VA_ARGS__) 23 | 24 | #ifndef DOXYGEN_IGNORE 25 | 26 | #define SMOLRTSP_PRIV_DEF_OPTION_1(T) \ 27 | datatype99(T##Option, (T##_Some, T), (T##_None)) 28 | #define SMOLRTSP_PRIV_DEF_OPTION_2(name, T) \ 29 | datatype99(name##Option, (name##_Some, T), (name##_None)) 30 | 31 | #endif // DOXYGEN_IGNORE 32 | -------------------------------------------------------------------------------- /tests/types/message_body.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST parse_message_body(void) { 6 | const CharSlice99 input = 7 | CharSlice99_from_str(" 012345 ~ abc(^*% D#NIN#3 "); 8 | const size_t content_length = input.len; 9 | 10 | SmolRTSP_MessageBody result; 11 | SmolRTSP_ParseResult ret; 12 | 13 | ret = SmolRTSP_MessageBody_parse(&result, input, content_length + 100); 14 | ASSERT(SmolRTSP_ParseResult_is_partial(ret)); 15 | ret = SmolRTSP_MessageBody_parse(&result, input, content_length + 3); 16 | ASSERT(SmolRTSP_ParseResult_is_partial(ret)); 17 | 18 | ret = SmolRTSP_MessageBody_parse(&result, input, content_length); 19 | ASSERT(SmolRTSP_ParseResult_is_complete(ret)); 20 | 21 | PASS(); 22 | } 23 | 24 | TEST empty(void) { 25 | ASSERT(CharSlice99_primitive_eq( 26 | SmolRTSP_MessageBody_empty(), CharSlice99_empty())); 27 | PASS(); 28 | } 29 | 30 | SUITE(types_message_body) { 31 | RUN_TEST(parse_message_body); 32 | RUN_TEST(empty); 33 | } 34 | -------------------------------------------------------------------------------- /include/smolrtsp/types/message_body.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP message body. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | /** 17 | * An RTSP message body. 18 | */ 19 | typedef CharSlice99 SmolRTSP_MessageBody; 20 | 21 | /** 22 | * Parses @p data to @p self. 23 | * 24 | * @pre `self != NULL` 25 | */ 26 | SmolRTSP_ParseResult SmolRTSP_MessageBody_parse( 27 | SmolRTSP_MessageBody *restrict self, CharSlice99 input, 28 | size_t content_length) SMOLRTSP_PRIV_MUST_USE; 29 | 30 | /** 31 | * Returns an empty message body. 32 | */ 33 | SmolRTSP_MessageBody SmolRTSP_MessageBody_empty(void) SMOLRTSP_PRIV_MUST_USE; 34 | 35 | /** 36 | * Tests @p lhs and @p rhs for equality. 37 | * 38 | * @pre `lhs != NULL` 39 | * @pre `rhs != NULL` 40 | */ 41 | bool SmolRTSP_MessageBody_eq( 42 | const SmolRTSP_MessageBody *restrict lhs, 43 | const SmolRTSP_MessageBody *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 44 | -------------------------------------------------------------------------------- /src/types/message_body.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | 7 | SmolRTSP_ParseResult SmolRTSP_MessageBody_parse( 8 | SmolRTSP_MessageBody *restrict self, CharSlice99 input, 9 | size_t content_length) { 10 | assert(self); 11 | 12 | if (input.len < content_length) { 13 | return SmolRTSP_ParseResult_partial(); 14 | } 15 | 16 | if (0 == content_length) { 17 | *self = CharSlice99_empty(); 18 | const size_t offset = 0; 19 | return SmolRTSP_ParseResult_complete(offset); 20 | } 21 | 22 | *self = CharSlice99_new(input.ptr, content_length); 23 | 24 | return SmolRTSP_ParseResult_complete(content_length); 25 | } 26 | 27 | SmolRTSP_MessageBody SmolRTSP_MessageBody_empty(void) { 28 | return CharSlice99_empty(); 29 | } 30 | 31 | bool SmolRTSP_MessageBody_eq( 32 | const SmolRTSP_MessageBody *restrict lhs, 33 | const SmolRTSP_MessageBody *restrict rhs) { 34 | assert(lhs); 35 | assert(rhs); 36 | 37 | return CharSlice99_primitive_eq(*lhs, *rhs); 38 | } 39 | -------------------------------------------------------------------------------- /src/types/sdp.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../macros.h" 4 | #include "parsing.h" 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | ssize_t SmolRTSP_SdpLine_serialize( 12 | const SmolRTSP_SdpLine *restrict self, SmolRTSP_Writer w) { 13 | assert(self); 14 | assert(w.self && w.vptr); 15 | 16 | return SMOLRTSP_WRITE_SLICES( 17 | w, { 18 | CharSlice99_new((char *)&self->ty, 1), 19 | CharSlice99_from_str("="), 20 | self->value, 21 | SMOLRTSP_CRLF, 22 | }); 23 | } 24 | 25 | ssize_t smolrtsp_sdp_printf( 26 | SmolRTSP_Writer w, SmolRTSP_SdpType ty, const char *fmt, ...) { 27 | assert(w.self && w.vptr); 28 | assert(fmt); 29 | 30 | ssize_t result = 0; 31 | 32 | va_list ap; 33 | va_start(ap, fmt); 34 | 35 | CHK_WRITE_ERR(result, VCALL(w, writef, "%c=", ty)); 36 | CHK_WRITE_ERR(result, VCALL(w, vwritef, fmt, ap)); 37 | CHK_WRITE_ERR(result, VCALL(w, write, SMOLRTSP_CRLF)); 38 | 39 | va_end(ap); 40 | 41 | return result; 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 OpenIPC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/cmake/modules/FindLibEvent.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find the LibEvent config processing library 2 | # Once done this will define 3 | # 4 | # LIBEVENT_FOUND - System has LibEvent 5 | # LIBEVENT_INCLUDE_DIR - the LibEvent include directory 6 | # LIBEVENT_LIBRARIES - The libraries needed to use LibEvent 7 | find_path (LIBEVENT_INCLUDE_DIR NAMES event.h) 8 | find_library (LIBEVENT_LIBRARY NAMES event) 9 | find_library (LIBEVENT_CORE NAMES event_core) 10 | find_library (LIBEVENT_EXTRA NAMES event_extra) 11 | if (NOT EVHTP_DISABLE_EVTHR) 12 | find_library (LIBEVENT_THREAD NAMES event_pthreads) 13 | endif() 14 | if (NOT EVHTP_DISABLE_SSL) 15 | find_library (LIBEVENT_SSL NAMES event_openssl) 16 | endif() 17 | include (FindPackageHandleStandardArgs) 18 | set (LIBEVENT_INCLUDE_DIRS ${LIBEVENT_INCLUDE_DIR}) 19 | set (LIBEVENT_LIBRARIES 20 | ${LIBEVENT_LIBRARY} 21 | ${LIBEVENT_SSL} 22 | ${LIBEVENT_CORE} 23 | ${LIBEVENT_EXTRA} 24 | ${LIBEVENT_THREAD} 25 | ${LIBEVENT_EXTRA}) 26 | find_package_handle_standard_args (LIBEVENT DEFAULT_MSG LIBEVENT_LIBRARIES LIBEVENT_INCLUDE_DIR) 27 | mark_as_advanced(LIBEVENT_INCLUDE_DIRS LIBEVENT_LIBRARIES) 28 | -------------------------------------------------------------------------------- /tests/types/method.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_Method) 7 | 8 | TEST parse_method(void) { 9 | TEST_PARSE("OPTIONS ", SMOLRTSP_METHOD_OPTIONS); 10 | TEST_PARSE("DESCRIBE ", SMOLRTSP_METHOD_DESCRIBE); 11 | TEST_PARSE("ANNOUNCE ", SMOLRTSP_METHOD_ANNOUNCE); 12 | TEST_PARSE("SETUP ", SMOLRTSP_METHOD_SETUP); 13 | TEST_PARSE("PLAY ", SMOLRTSP_METHOD_PLAY); 14 | TEST_PARSE("PAUSE ", SMOLRTSP_METHOD_PAUSE); 15 | TEST_PARSE("TEARDOWN ", SMOLRTSP_METHOD_TEARDOWN); 16 | TEST_PARSE("GET_PARAMETER ", SMOLRTSP_METHOD_GET_PARAMETER); 17 | TEST_PARSE("SET_PARAMETER ", SMOLRTSP_METHOD_SET_PARAMETER); 18 | TEST_PARSE("REDIRECT ", SMOLRTSP_METHOD_REDIRECT); 19 | TEST_PARSE("RECORD ", SMOLRTSP_METHOD_RECORD); 20 | 21 | SmolRTSP_Method result; 22 | 23 | ASSERT(SmolRTSP_ParseResult_is_failure( 24 | SmolRTSP_Method_parse(&result, CharSlice99_from_str("~123")))); 25 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_Method_parse( 26 | &result, CharSlice99_from_str("/ hello ~19r world")))); 27 | 28 | PASS(); 29 | } 30 | 31 | SUITE(types_method) { 32 | RUN_TEST(parse_method); 33 | } 34 | -------------------------------------------------------------------------------- /src/writer/fd.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | typedef int FdWriter; 6 | 7 | static ssize_t FdWriter_write(VSelf, CharSlice99 data) { 8 | VSELF(FdWriter); 9 | assert(self); 10 | 11 | return write(*self, data.ptr, data.len); 12 | } 13 | 14 | static void FdWriter_lock(VSelf) { 15 | VSELF(FdWriter); 16 | (void)self; 17 | } 18 | 19 | static void FdWriter_unlock(VSelf) { 20 | VSELF(FdWriter); 21 | (void)self; 22 | } 23 | 24 | static size_t FdWriter_filled(VSelf) { 25 | VSELF(FdWriter); 26 | (void)self; 27 | return 0; 28 | } 29 | 30 | static int FdWriter_vwritef(VSelf, const char *restrict fmt, va_list ap) { 31 | VSELF(FdWriter); 32 | 33 | assert(self); 34 | assert(fmt); 35 | 36 | return vdprintf(*self, fmt, ap); 37 | } 38 | 39 | static int FdWriter_writef(VSelf, const char *restrict fmt, ...) { 40 | VSELF(FdWriter); 41 | 42 | assert(self); 43 | assert(fmt); 44 | 45 | va_list ap; 46 | va_start(ap, fmt); 47 | 48 | const int ret = FdWriter_vwritef(self, fmt, ap); 49 | va_end(ap); 50 | 51 | return ret; 52 | } 53 | 54 | impl(SmolRTSP_Writer, FdWriter); 55 | 56 | SmolRTSP_Writer smolrtsp_fd_writer(int *fd) { 57 | assert(fd); 58 | return DYN(FdWriter, SmolRTSP_Writer, fd); 59 | } 60 | -------------------------------------------------------------------------------- /tests/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Check that the main header compiles well. 4 | #include 5 | 6 | #define SMOLRTSP_SUITE(name) \ 7 | extern void name(void); \ 8 | RUN_SUITE(name) 9 | 10 | GREATEST_MAIN_DEFS(); 11 | 12 | int main(int argc, char *argv[]) { 13 | GREATEST_MAIN_BEGIN(); 14 | 15 | SMOLRTSP_SUITE(types_header_map); 16 | SMOLRTSP_SUITE(types_header); 17 | SMOLRTSP_SUITE(types_message_body); 18 | SMOLRTSP_SUITE(types_method); 19 | SMOLRTSP_SUITE(types_reason_phrase); 20 | SMOLRTSP_SUITE(types_request_line); 21 | SMOLRTSP_SUITE(types_request_uri); 22 | SMOLRTSP_SUITE(types_request); 23 | SMOLRTSP_SUITE(types_response_line); 24 | SMOLRTSP_SUITE(types_response); 25 | SMOLRTSP_SUITE(types_rtsp_version); 26 | SMOLRTSP_SUITE(types_sdp); 27 | SMOLRTSP_SUITE(types_status_code); 28 | 29 | SMOLRTSP_SUITE(nal_h264); 30 | SMOLRTSP_SUITE(nal_h265); 31 | SMOLRTSP_SUITE(nal); 32 | 33 | SMOLRTSP_SUITE(util); 34 | SMOLRTSP_SUITE(writer); 35 | SMOLRTSP_SUITE(transport); 36 | SMOLRTSP_SUITE(io_vec); 37 | SMOLRTSP_SUITE(context); 38 | SMOLRTSP_SUITE(controller); 39 | 40 | GREATEST_MAIN_END(); 41 | } 42 | -------------------------------------------------------------------------------- /tests/types/header.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_Header) 7 | 8 | TEST parse_header(void) { 9 | TEST_PARSE( 10 | "User-Agent: LibVLC/3.0.8 (LIVE555 Streaming Media v2018.02.18)\r\n", 11 | ((SmolRTSP_Header){ 12 | SMOLRTSP_HEADER_USER_AGENT, 13 | CharSlice99_from_str( 14 | "LibVLC/3.0.8 (LIVE555 Streaming Media v2018.02.18)"), 15 | })); 16 | 17 | SmolRTSP_Header result; 18 | 19 | ASSERT(SmolRTSP_ParseResult_is_failure( 20 | SmolRTSP_Header_parse(&result, CharSlice99_from_str("~@~")))); 21 | 22 | PASS(); 23 | } 24 | 25 | TEST serialize_header(void) { 26 | char buffer[200] = {0}; 27 | 28 | const SmolRTSP_Header header = { 29 | SMOLRTSP_HEADER_CONTENT_LENGTH, 30 | CharSlice99_from_str("123"), 31 | }; 32 | 33 | const ssize_t ret = 34 | SmolRTSP_Header_serialize(&header, smolrtsp_string_writer(buffer)); 35 | 36 | const char *expected = "Content-Length: 123\r\n"; 37 | 38 | ASSERT_EQ((ssize_t)strlen(expected), ret); 39 | ASSERT_STR_EQ(expected, buffer); 40 | 41 | PASS(); 42 | } 43 | 44 | SUITE(types_header) { 45 | RUN_TEST(parse_header); 46 | RUN_TEST(serialize_header); 47 | } 48 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(examples LANGUAGES C) 3 | 4 | add_subdirectory(.. build) 5 | 6 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") 7 | 8 | find_package(LibEvent REQUIRED) 9 | 10 | include(FetchContent) 11 | 12 | FetchContent_Declare( 13 | smolrtsp-libevent 14 | GIT_REPOSITORY https://github.com/OpenIPC/smolrtsp-libevent.git 15 | GIT_TAG 369b1a38688122521552dee6023fcbdb9cf6aa6b 16 | ) 17 | 18 | FetchContent_MakeAvailable(smolrtsp-libevent) 19 | 20 | add_executable(server server.c) 21 | 22 | target_link_libraries(server smolrtsp smolrtsp-libevent ${LIBEVENT_LIBRARIES}) 23 | target_include_directories(server PRIVATE ${LIBEVENT_INCLUDE_DIR}) 24 | set_target_properties(server PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) 25 | 26 | if(CMAKE_C_COMPILER_ID STREQUAL "Clang") 27 | target_compile_options(server PRIVATE -Wall -Wextra -fsanitize=address) 28 | elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU") 29 | target_compile_options(server PRIVATE -Wall -Wextra -fsanitize=address -Wno-misleading-indentation) 30 | endif() 31 | 32 | target_link_options(server PRIVATE -fsanitize=address) 33 | 34 | execute_process(COMMAND xxd -i ../media/audio.g711a ../media/audio.g711a.h) 35 | execute_process(COMMAND xxd -i ../media/video.h264 ../media/video.h264.h) 36 | -------------------------------------------------------------------------------- /tests/types/rtsp_version.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_RtspVersion) 7 | 8 | TEST parse_rtsp_version(void) { 9 | TEST_PARSE("RTSP/1.1 ", ((SmolRTSP_RtspVersion){1, 1})); 10 | TEST_PARSE("RTSP/0.0 ", ((SmolRTSP_RtspVersion){0, 0})); 11 | TEST_PARSE("RTSP/123.200 ", ((SmolRTSP_RtspVersion){123, 200})); 12 | TEST_PARSE("RTSP/0.200 ", ((SmolRTSP_RtspVersion){0, 200})); 13 | 14 | SmolRTSP_RtspVersion result; 15 | 16 | ASSERT(SmolRTSP_ParseResult_is_failure( 17 | SmolRTSP_RtspVersion_parse(&result, CharSlice99_from_str("192")))); 18 | ASSERT(SmolRTSP_ParseResult_is_failure( 19 | SmolRTSP_RtspVersion_parse(&result, CharSlice99_from_str(" ~ RTSP/")))); 20 | 21 | PASS(); 22 | } 23 | 24 | TEST serialize_rtsp_version(void) { 25 | char buffer[20] = {0}; 26 | 27 | const ssize_t ret = SmolRTSP_RtspVersion_serialize( 28 | &(SmolRTSP_RtspVersion){1, 0}, smolrtsp_string_writer(buffer)); 29 | 30 | const char *expected = "RTSP/1.0"; 31 | 32 | ASSERT_EQ((ssize_t)strlen(expected), ret); 33 | ASSERT_STR_EQ(expected, buffer); 34 | 35 | PASS(); 36 | } 37 | 38 | SUITE(types_rtsp_version) { 39 | RUN_TEST(parse_rtsp_version); 40 | RUN_TEST(serialize_rtsp_version); 41 | } 42 | -------------------------------------------------------------------------------- /tests/types/status_code.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_StatusCode) 7 | 8 | TEST parse_status_code(void) { 9 | TEST_PARSE("100 ", SMOLRTSP_STATUS_CONTINUE); 10 | TEST_PARSE("200 ", SMOLRTSP_STATUS_OK); 11 | TEST_PARSE("303 ", SMOLRTSP_STATUS_SEE_OTHER); 12 | TEST_PARSE("404 ", SMOLRTSP_STATUS_NOT_FOUND); 13 | TEST_PARSE("551 ", SMOLRTSP_STATUS_OPTION_NOT_SUPPORTED); 14 | 15 | SmolRTSP_StatusCode result; 16 | 17 | ASSERT(SmolRTSP_ParseResult_is_failure( 18 | SmolRTSP_StatusCode_parse(&result, CharSlice99_from_str("blah")))); 19 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_StatusCode_parse( 20 | &result, CharSlice99_from_str("~ 2424 blah")))); 21 | 22 | PASS(); 23 | } 24 | 25 | TEST serialize_status_code(void) { 26 | char buffer[20] = {0}; 27 | 28 | const SmolRTSP_StatusCode status = SMOLRTSP_STATUS_NOT_FOUND; 29 | 30 | const ssize_t ret = 31 | SmolRTSP_StatusCode_serialize(&status, smolrtsp_string_writer(buffer)); 32 | 33 | const char *expected = "404"; 34 | 35 | ASSERT_EQ((ssize_t)strlen(expected), ret); 36 | ASSERT_STR_EQ(expected, buffer); 37 | 38 | PASS(); 39 | } 40 | 41 | SUITE(types_status_code) { 42 | RUN_TEST(parse_status_code); 43 | RUN_TEST(serialize_status_code); 44 | } 45 | -------------------------------------------------------------------------------- /src/writer/string.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | typedef char StringWriter; 7 | 8 | static ssize_t StringWriter_write(VSelf, CharSlice99 data) { 9 | VSELF(StringWriter); 10 | assert(self); 11 | 12 | strncat(self, data.ptr, data.len); 13 | return data.len; 14 | } 15 | 16 | static void StringWriter_lock(VSelf) { 17 | VSELF(StringWriter); 18 | (void)self; 19 | } 20 | 21 | static void StringWriter_unlock(VSelf) { 22 | VSELF(StringWriter); 23 | (void)self; 24 | } 25 | 26 | static size_t StringWriter_filled(VSelf) { 27 | VSELF(StringWriter); 28 | (void)self; 29 | return 0; 30 | } 31 | 32 | static int StringWriter_vwritef(VSelf, const char *restrict fmt, va_list ap) { 33 | VSELF(StringWriter); 34 | 35 | assert(self); 36 | assert(fmt); 37 | 38 | return vsprintf(self + strlen(self), fmt, ap); 39 | } 40 | 41 | static int StringWriter_writef(VSelf, const char *restrict fmt, ...) { 42 | VSELF(StringWriter); 43 | 44 | assert(self); 45 | assert(fmt); 46 | 47 | va_list ap; 48 | va_start(ap, fmt); 49 | 50 | const int ret = StringWriter_vwritef(self, fmt, ap); 51 | va_end(ap); 52 | 53 | return ret; 54 | } 55 | 56 | impl(SmolRTSP_Writer, StringWriter); 57 | 58 | SmolRTSP_Writer smolrtsp_string_writer(char *buffer) { 59 | assert(buffer); 60 | return DYN(StringWriter, SmolRTSP_Writer, buffer); 61 | } 62 | -------------------------------------------------------------------------------- /src/types/status_code.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | ssize_t SmolRTSP_StatusCode_serialize( 13 | const SmolRTSP_StatusCode *restrict self, SmolRTSP_Writer w) { 14 | assert(self); 15 | assert(w.self && w.vptr); 16 | 17 | return VCALL(w, writef, "%" PRIu16, *self); 18 | } 19 | 20 | SmolRTSP_ParseResult SmolRTSP_StatusCode_parse( 21 | SmolRTSP_StatusCode *restrict self, CharSlice99 input) { 22 | assert(self); 23 | 24 | const CharSlice99 backup = input; 25 | 26 | MATCH(smolrtsp_match_whitespaces(input)); 27 | CharSlice99 code = input; 28 | MATCH(smolrtsp_match_numeric(input)); 29 | code = CharSlice99_from_ptrdiff(code.ptr, input.ptr); 30 | 31 | SmolRTSP_StatusCode code_int; 32 | if (sscanf(CharSlice99_alloca_c_str(code), "%" SCNu16, &code_int) != 1) { 33 | return SmolRTSP_ParseResult_Failure( 34 | SmolRTSP_ParseError_TypeMismatch(SmolRTSP_ParseType_Int, code)); 35 | } 36 | 37 | *self = code_int; 38 | 39 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 40 | } 41 | 42 | bool SmolRTSP_StatusCode_eq( 43 | const SmolRTSP_StatusCode *restrict lhs, 44 | const SmolRTSP_StatusCode *restrict rhs) { 45 | assert(lhs); 46 | assert(rhs); 47 | 48 | return *lhs == *rhs; 49 | } 50 | -------------------------------------------------------------------------------- /include/smolrtsp.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Re-exports all the functionality. 4 | */ 5 | 6 | /** 7 | * @mainpage 8 | * 9 | * A small, portable, extensible RTSP 1.0 implementation in C99.
10 | * 11 | * See the file list for the available abstractions. 12 | * See our official GitHub 13 | * repository for a high-level overview. 14 | */ 15 | 16 | #pragma once 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | -------------------------------------------------------------------------------- /src/writer/file.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | typedef FILE FileWriter; 7 | 8 | static ssize_t FileWriter_write(VSelf, CharSlice99 data) { 9 | VSELF(FileWriter); 10 | assert(self); 11 | 12 | const size_t ret = fwrite(data.ptr, sizeof(char), data.len, self); 13 | if (ferror(self)) { 14 | return -1; 15 | } 16 | 17 | return ret; 18 | } 19 | 20 | static void FileWriter_lock(VSelf) { 21 | VSELF(FileWriter); 22 | (void)self; 23 | } 24 | 25 | static void FileWriter_unlock(VSelf) { 26 | VSELF(FileWriter); 27 | (void)self; 28 | } 29 | 30 | static size_t FileWriter_filled(VSelf) { 31 | VSELF(FileWriter); 32 | (void)self; 33 | return 0; 34 | } 35 | 36 | static int FileWriter_vwritef(VSelf, const char *restrict fmt, va_list ap) { 37 | VSELF(FileWriter); 38 | 39 | assert(self); 40 | assert(fmt); 41 | 42 | return vfprintf(self, fmt, ap); 43 | } 44 | 45 | static int FileWriter_writef(VSelf, const char *restrict fmt, ...) { 46 | VSELF(FileWriter); 47 | 48 | assert(self); 49 | assert(fmt); 50 | 51 | va_list ap; 52 | va_start(ap, fmt); 53 | 54 | const int ret = FileWriter_vwritef(self, fmt, ap); 55 | va_end(ap); 56 | 57 | return ret; 58 | } 59 | 60 | impl(SmolRTSP_Writer, FileWriter); 61 | 62 | SmolRTSP_Writer smolrtsp_file_writer(FILE *stream) { 63 | assert(stream); 64 | return DYN(FileWriter, SmolRTSP_Writer, stream); 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## unreleased 8 | 9 | ### Fixed 10 | 11 | - Update the minimum required CMake version to 3.10.0 due to deprecation (see [metalang99/issues/33](https://github.com/hirrolot/metalang99/issues/33)). 12 | 13 | ### Fixed 14 | 15 | - Fix the `SmolRTSP_NalTransportConfig_default` value for H.265 ([PR #17](https://github.com/OpenIPC/smolrtsp/pull/17)). 16 | 17 | ## 0.1.3 - 2023-03-12 18 | 19 | ### Fixed 20 | 21 | - Fix the `DOWNLOAD_EXTRACT_TIMESTAMP` CMake warning (see [datatype99/issues/15](https://github.com/hirrolot/datatype99/issues/15)). 22 | 23 | ## 0.1.2 - 2022-07-27 24 | 25 | ### Fixed 26 | 27 | - Suppress a compilation warning for an unused variable in `smolrtsp_vheader`. 28 | - Overflow while computing an RTP timestamp. 29 | 30 | ## 0.1.1 - 2022-03-31 31 | 32 | ### Fixed 33 | 34 | - Mark the following functions with `__attribute__((warn_unused_result))` (when available): 35 | - `SmolRTSP_ParseError_print`. 36 | - `SmolRTSP_MessageBody_empty`. 37 | - `SmolRTSP_Request_uninit`. 38 | - `SmolRTSP_Response_uninit`. 39 | - `SmolRTSP_NalTransportConfig_default`. 40 | - `smolrtsp_determine_start_code`. 41 | - `smolrtsp_dgram_socket`. 42 | 43 | ## 0.1.0 - 2022-03-30 44 | 45 | ### Added 46 | 47 | - This awesome library. 48 | -------------------------------------------------------------------------------- /include/smolrtsp/types/rtsp_version.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP version. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | /** 18 | * An RTSP version. 19 | */ 20 | typedef struct { 21 | /** 22 | * The major number. 23 | */ 24 | uint8_t major; 25 | 26 | /** 27 | * The minor number. 28 | */ 29 | uint8_t minor; 30 | } SmolRTSP_RtspVersion; 31 | 32 | /** 33 | * Serialises @p self into @p w. 34 | * 35 | * @param[in] self The instance to be serialised. 36 | * @param[in] w The writer to be provided with serialised data. 37 | * 38 | * @return The number of bytes written or a negative value on error. 39 | * 40 | * @pre `self != NULL` 41 | * @pre `w.self && w.vptr` 42 | */ 43 | ssize_t SmolRTSP_RtspVersion_serialize( 44 | const SmolRTSP_RtspVersion *restrict self, 45 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 46 | 47 | /** 48 | * Parses @p data to @p self. 49 | * 50 | * @pre `self != NULL` 51 | */ 52 | SmolRTSP_ParseResult SmolRTSP_RtspVersion_parse( 53 | SmolRTSP_RtspVersion *restrict self, 54 | CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 55 | 56 | /** 57 | * Tests @p lhs and @p rhs for equality. 58 | * 59 | * @pre `lhs != NULL` 60 | * @pre `rhs != NULL` 61 | */ 62 | bool SmolRTSP_RtspVersion_eq( 63 | const SmolRTSP_RtspVersion *restrict lhs, 64 | const SmolRTSP_RtspVersion *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 65 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(tests LANGUAGES C) 3 | 4 | # Fix the warnings about `DOWNLOAD_EXTRACT_TIMESTAMP` in newer CMake versions. 5 | if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 6 | cmake_policy(SET CMP0135 NEW) 7 | endif() 8 | 9 | add_subdirectory(.. build) 10 | 11 | include(FetchContent) 12 | 13 | FetchContent_Declare( 14 | greatest 15 | URL https://github.com/silentbicycle/greatest/archive/refs/tags/v1.5.0.tar.gz) 16 | 17 | FetchContent_MakeAvailable(greatest) 18 | FetchContent_GetProperties(greatest) 19 | 20 | add_executable( 21 | tests 22 | main.c 23 | types/header_map.c 24 | types/header.c 25 | types/message_body.c 26 | types/method.c 27 | types/reason_phrase.c 28 | types/request.c 29 | types/request_line.c 30 | types/response.c 31 | types/response_line.c 32 | types/request_uri.c 33 | types/rtsp_version.c 34 | types/status_code.c 35 | types/sdp.c 36 | types/test_util.h 37 | nal/h264.c 38 | nal/h265.c 39 | nal.c 40 | util.c 41 | writer.c 42 | io_vec.c 43 | controller.c 44 | context.c 45 | transport.c) 46 | 47 | if(CMAKE_C_COMPILER_ID STREQUAL "Clang") 48 | target_compile_options(tests PRIVATE -Wall -Wextra -fsanitize=address) 49 | elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU") 50 | target_compile_options(tests PRIVATE -Wall -Wextra -fsanitize=address -Wno-misleading-indentation) 51 | endif() 52 | 53 | target_link_options(tests PRIVATE -fsanitize=address) 54 | 55 | target_link_libraries(tests smolrtsp) 56 | target_include_directories(tests PRIVATE ${greatest_SOURCE_DIR}) 57 | 58 | set_target_properties(tests PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) 59 | -------------------------------------------------------------------------------- /tests/types/request_line.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_RequestLine) 7 | 8 | TEST parse_request_line(void) { 9 | const SmolRTSP_RequestLine expected = { 10 | .method = SMOLRTSP_METHOD_DESCRIBE, 11 | .uri = CharSlice99_from_str("http://example.com"), 12 | .version = {.major = 1, .minor = 1}, 13 | }; 14 | 15 | TEST_PARSE("DESCRIBE http://example.com RTSP/1.1\r\n", expected); 16 | 17 | SmolRTSP_RequestLine result; 18 | 19 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_RequestLine_parse( 20 | &result, CharSlice99_from_str("!!! http://example.com RTSP/1.1\r\n")))); 21 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_RequestLine_parse( 22 | &result, CharSlice99_from_str( 23 | "DESCRIBE http://example.com ABRACADABRA/1.1\r\n")))); 24 | 25 | PASS(); 26 | } 27 | 28 | TEST serialize_request_line(void) { 29 | char buffer[100] = {0}; 30 | 31 | const SmolRTSP_RequestLine line = { 32 | .method = SMOLRTSP_METHOD_DESCRIBE, 33 | .uri = CharSlice99_from_str("http://example.com"), 34 | .version = (SmolRTSP_RtspVersion){1, 0}, 35 | }; 36 | 37 | const ssize_t ret = 38 | SmolRTSP_RequestLine_serialize(&line, smolrtsp_string_writer(buffer)); 39 | 40 | const char *expected = "DESCRIBE http://example.com RTSP/1.0\r\n"; 41 | 42 | ASSERT_EQ((ssize_t)strlen(expected), ret); 43 | ASSERT_STR_EQ(expected, buffer); 44 | 45 | PASS(); 46 | } 47 | 48 | SUITE(types_request_line) { 49 | RUN_TEST(parse_request_line); 50 | RUN_TEST(serialize_request_line); 51 | } 52 | -------------------------------------------------------------------------------- /src/controller.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | void smolrtsp_dispatch( 6 | SmolRTSP_Writer conn, SmolRTSP_Controller controller, 7 | const SmolRTSP_Request *restrict req) { 8 | assert(conn.self && conn.vptr); 9 | assert(controller.self && controller.vptr); 10 | assert(req); 11 | 12 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(conn, req->cseq); 13 | 14 | if (VCALL(controller, before, ctx, req) == SmolRTSP_ControlFlow_Break) { 15 | goto after; 16 | } 17 | 18 | const SmolRTSP_Method method = req->start_line.method, 19 | options = SMOLRTSP_METHOD_OPTIONS, 20 | describe = SMOLRTSP_METHOD_DESCRIBE, 21 | setup = SMOLRTSP_METHOD_SETUP, 22 | play = SMOLRTSP_METHOD_PLAY, 23 | teardown = SMOLRTSP_METHOD_TEARDOWN; 24 | 25 | if (SmolRTSP_Method_eq(&method, &options)) { 26 | VCALL(controller, options, ctx, req); 27 | } else if (SmolRTSP_Method_eq(&method, &describe)) { 28 | VCALL(controller, describe, ctx, req); 29 | } else if (SmolRTSP_Method_eq(&method, &setup)) { 30 | VCALL(controller, setup, ctx, req); 31 | } else if (SmolRTSP_Method_eq(&method, &play)) { 32 | VCALL(controller, play, ctx, req); 33 | } else if (SmolRTSP_Method_eq(&method, &teardown)) { 34 | VCALL(controller, teardown, ctx, req); 35 | } else { 36 | VCALL(controller, unknown, ctx, req); 37 | } 38 | 39 | after: 40 | VCALL(controller, after, SmolRTSP_Context_get_ret(ctx), ctx, req); 41 | 42 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 43 | } 44 | -------------------------------------------------------------------------------- /tests/types/response_line.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "test_util.h" 4 | #include 5 | 6 | DEF_TEST_PARSE(SmolRTSP_ResponseLine) 7 | 8 | TEST parse_response_line(void) { 9 | const SmolRTSP_ResponseLine expected = { 10 | .version = {.major = 1, .minor = 1}, 11 | .code = SMOLRTSP_STATUS_OK, 12 | .reason = CharSlice99_from_str("OK"), 13 | }; 14 | 15 | TEST_PARSE("RTSP/1.1 200 OK\r\n", expected); 16 | 17 | SmolRTSP_ResponseLine result; 18 | 19 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_ResponseLine_parse( 20 | &result, CharSlice99_from_str("ABRACADABRA/1.1 200 OK\r\n")))); 21 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_ResponseLine_parse( 22 | &result, CharSlice99_from_str("RTSP/42 200 OK\r\n")))); 23 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_ResponseLine_parse( 24 | &result, CharSlice99_from_str("RTSP/1.1 ~~~ OK\r\n")))); 25 | 26 | PASS(); 27 | } 28 | 29 | TEST serialize_response_line(void) { 30 | char buffer[100] = {0}; 31 | 32 | const SmolRTSP_ResponseLine line = { 33 | .version = (SmolRTSP_RtspVersion){1, 0}, 34 | .code = SMOLRTSP_STATUS_OK, 35 | .reason = CharSlice99_from_str("OK"), 36 | }; 37 | 38 | const ssize_t ret = 39 | SmolRTSP_ResponseLine_serialize(&line, smolrtsp_string_writer(buffer)); 40 | 41 | const char *expected = "RTSP/1.0 200 OK\r\n"; 42 | 43 | ASSERT_EQ((ssize_t)strlen(expected), ret); 44 | ASSERT_STR_EQ(expected, buffer); 45 | 46 | PASS(); 47 | } 48 | 49 | SUITE(types_response_line) { 50 | RUN_TEST(parse_response_line); 51 | RUN_TEST(serialize_response_line); 52 | } 53 | -------------------------------------------------------------------------------- /src/types/header.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | ssize_t SmolRTSP_Header_serialize( 10 | const SmolRTSP_Header *restrict self, SmolRTSP_Writer w) { 11 | assert(self); 12 | assert(w.self && w.vptr); 13 | 14 | return SMOLRTSP_WRITE_SLICES( 15 | w, { 16 | self->key, 17 | CharSlice99_from_str(": "), 18 | self->value, 19 | SMOLRTSP_CRLF, 20 | }); 21 | } 22 | 23 | SmolRTSP_ParseResult 24 | SmolRTSP_Header_parse(SmolRTSP_Header *restrict self, CharSlice99 input) { 25 | assert(self); 26 | 27 | const CharSlice99 backup = input; 28 | 29 | SmolRTSP_Header header; 30 | 31 | MATCH(smolrtsp_match_whitespaces(input)); 32 | header.key = input; 33 | MATCH(smolrtsp_match_header_name(input)); 34 | header.key = CharSlice99_from_ptrdiff(header.key.ptr, input.ptr); 35 | 36 | MATCH(smolrtsp_match_whitespaces(input)); 37 | MATCH(smolrtsp_match_char(input, ':')); 38 | MATCH(smolrtsp_match_whitespaces(input)); 39 | 40 | header.value = input; 41 | MATCH(smolrtsp_match_until_crlf(input)); 42 | header.value = 43 | CharSlice99_from_ptrdiff(header.value.ptr, input.ptr - strlen("\r\n")); 44 | 45 | *self = header; 46 | 47 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 48 | } 49 | 50 | bool SmolRTSP_Header_eq( 51 | const SmolRTSP_Header *restrict lhs, const SmolRTSP_Header *restrict rhs) { 52 | assert(lhs); 53 | assert(rhs); 54 | 55 | return CharSlice99_primitive_eq(lhs->key, rhs->key) && 56 | CharSlice99_primitive_eq(lhs->value, rhs->value); 57 | } 58 | -------------------------------------------------------------------------------- /src/types/response_line.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../macros.h" 4 | #include "parsing.h" 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | ssize_t SmolRTSP_ResponseLine_serialize( 11 | const SmolRTSP_ResponseLine *restrict self, SmolRTSP_Writer w) { 12 | assert(self); 13 | assert(w.self && w.vptr); 14 | 15 | ssize_t result = 0; 16 | 17 | CHK_WRITE_ERR(result, SmolRTSP_RtspVersion_serialize(&self->version, w)); 18 | CHK_WRITE_ERR(result, VCALL(w, write, CharSlice99_from_str(" "))); 19 | CHK_WRITE_ERR(result, SmolRTSP_StatusCode_serialize(&self->code, w)); 20 | CHK_WRITE_ERR(result, VCALL(w, write, CharSlice99_from_str(" "))); 21 | CHK_WRITE_ERR(result, VCALL(w, write, self->reason)); 22 | CHK_WRITE_ERR(result, VCALL(w, write, SMOLRTSP_CRLF)); 23 | 24 | return result; 25 | } 26 | 27 | SmolRTSP_ParseResult SmolRTSP_ResponseLine_parse( 28 | SmolRTSP_ResponseLine *restrict self, CharSlice99 input) { 29 | assert(self); 30 | 31 | const CharSlice99 backup = input; 32 | 33 | MATCH(SmolRTSP_RtspVersion_parse(&self->version, input)); 34 | MATCH(SmolRTSP_StatusCode_parse(&self->code, input)); 35 | MATCH(SmolRTSP_ReasonPhrase_parse(&self->reason, input)); 36 | 37 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 38 | } 39 | 40 | bool SmolRTSP_ResponseLine_eq( 41 | const SmolRTSP_ResponseLine *restrict lhs, 42 | const SmolRTSP_ResponseLine *restrict rhs) { 43 | assert(lhs); 44 | assert(rhs); 45 | 46 | return SmolRTSP_RtspVersion_eq(&lhs->version, &rhs->version) && 47 | lhs->code == rhs->code && 48 | SmolRTSP_ReasonPhrase_eq(&lhs->reason, &rhs->reason); 49 | } 50 | -------------------------------------------------------------------------------- /tests/nal/h264.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST parse(void) { 6 | const SmolRTSP_H264NalHeader h = SmolRTSP_H264NalHeader_parse(0b01011010); 7 | 8 | ASSERT(!h.forbidden_zero_bit); 9 | ASSERT_EQ(0b10, h.ref_idc); 10 | ASSERT_EQ(0b11010, h.unit_type); 11 | 12 | PASS(); 13 | } 14 | 15 | TEST serialize(void) { 16 | const SmolRTSP_H264NalHeader h = { 17 | .forbidden_zero_bit = false, 18 | .ref_idc = 0b10, 19 | .unit_type = 0b11010, 20 | }; 21 | 22 | const uint8_t ret = SmolRTSP_H264NalHeader_serialize(h); 23 | ASSERT_EQ(0b01011010, ret); 24 | 25 | PASS(); 26 | } 27 | 28 | TEST write_fu_header(void) { 29 | const SmolRTSP_H264NalHeader h = { 30 | .forbidden_zero_bit = false, 31 | .ref_idc = 0b10, 32 | .unit_type = 0b11010, 33 | }; 34 | 35 | uint8_t buffer[SMOLRTSP_H264_FU_HEADER_SIZE] = {0}; 36 | 37 | #define CHECK(is_first_fragment, is_last_fragment, ...) \ 38 | do { \ 39 | SmolRTSP_H264NalHeader_write_fu_header( \ 40 | h, buffer, is_first_fragment, is_last_fragment); \ 41 | ASSERT_MEM_EQ(((uint8_t[]){__VA_ARGS__}), buffer, sizeof buffer); \ 42 | } while (0) 43 | 44 | CHECK(true, true, 0b01011100, 0b11011010); 45 | CHECK(true, false, 0b01011100, 0b10011010); 46 | CHECK(false, true, 0b01011100, 0b01011010); 47 | CHECK(false, false, 0b01011100, 0b00011010); 48 | 49 | #undef CHECK 50 | 51 | PASS(); 52 | } 53 | 54 | SUITE(nal_h264) { 55 | RUN_TEST(parse); 56 | RUN_TEST(serialize); 57 | RUN_TEST(write_fu_header); 58 | } 59 | -------------------------------------------------------------------------------- /src/types/request_line.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../macros.h" 4 | #include "parsing.h" 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | ssize_t SmolRTSP_RequestLine_serialize( 11 | const SmolRTSP_RequestLine *restrict self, SmolRTSP_Writer w) { 12 | assert(self); 13 | assert(w.self && w.vptr); 14 | 15 | ssize_t result = 0; 16 | 17 | CHK_WRITE_ERR(result, VCALL(w, write, self->method)); 18 | CHK_WRITE_ERR(result, VCALL(w, write, CharSlice99_from_str(" "))); 19 | CHK_WRITE_ERR(result, VCALL(w, write, self->uri)); 20 | CHK_WRITE_ERR(result, VCALL(w, write, CharSlice99_from_str(" "))); 21 | CHK_WRITE_ERR(result, SmolRTSP_RtspVersion_serialize(&self->version, w)); 22 | CHK_WRITE_ERR(result, VCALL(w, write, SMOLRTSP_CRLF)); 23 | 24 | return result; 25 | } 26 | 27 | SmolRTSP_ParseResult SmolRTSP_RequestLine_parse( 28 | SmolRTSP_RequestLine *restrict self, CharSlice99 input) { 29 | assert(self); 30 | 31 | const CharSlice99 backup = input; 32 | 33 | MATCH(SmolRTSP_Method_parse(&self->method, input)); 34 | MATCH(SmolRTSP_RequestUri_parse(&self->uri, input)); 35 | MATCH(SmolRTSP_RtspVersion_parse(&self->version, input)); 36 | MATCH(smolrtsp_match_str(input, "\r\n")); 37 | 38 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 39 | } 40 | 41 | bool SmolRTSP_RequestLine_eq( 42 | const SmolRTSP_RequestLine *restrict lhs, 43 | const SmolRTSP_RequestLine *restrict rhs) { 44 | assert(lhs); 45 | assert(rhs); 46 | 47 | return SmolRTSP_Method_eq(&lhs->method, &rhs->method) && 48 | SmolRTSP_RequestUri_eq(&lhs->uri, &rhs->uri) && 49 | SmolRTSP_RtspVersion_eq(&lhs->version, &rhs->version); 50 | } 51 | -------------------------------------------------------------------------------- /include/smolrtsp/types/request_line.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP request line. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * An RTSP request line. 20 | */ 21 | typedef struct { 22 | /** 23 | * The method used. 24 | */ 25 | SmolRTSP_Method method; 26 | 27 | /** 28 | * The request URI. 29 | */ 30 | SmolRTSP_RequestUri uri; 31 | 32 | /** 33 | * The RTSP version used. 34 | */ 35 | SmolRTSP_RtspVersion version; 36 | } SmolRTSP_RequestLine; 37 | 38 | /** 39 | * Serialises @p self into @p w. 40 | * 41 | * @param[in] self The instance to be serialised. 42 | * @param[in] w The writer to be provided with serialised data. 43 | * 44 | * @return The number of bytes written or a negative value on error. 45 | * 46 | * @pre `self != NULL` 47 | * @pre `w.self && w.vptr` 48 | */ 49 | ssize_t SmolRTSP_RequestLine_serialize( 50 | const SmolRTSP_RequestLine *restrict self, 51 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 52 | 53 | /** 54 | * Parses @p data to @p self. 55 | * 56 | * @pre `self != NULL` 57 | */ 58 | SmolRTSP_ParseResult SmolRTSP_RequestLine_parse( 59 | SmolRTSP_RequestLine *restrict self, 60 | CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 61 | 62 | /** 63 | * Tests @p lhs and @p rhs for equality. 64 | * 65 | * @pre `lhs != NULL` 66 | * @pre `rhs != NULL` 67 | */ 68 | bool SmolRTSP_RequestLine_eq( 69 | const SmolRTSP_RequestLine *restrict lhs, 70 | const SmolRTSP_RequestLine *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 71 | -------------------------------------------------------------------------------- /include/smolrtsp/types/response_line.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP response line. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * An RTSP response line. 20 | */ 21 | typedef struct { 22 | /** 23 | * The RTSP version used. 24 | */ 25 | SmolRTSP_RtspVersion version; 26 | 27 | /** 28 | * The status code. 29 | */ 30 | SmolRTSP_StatusCode code; 31 | 32 | /** 33 | * The reason phrase. 34 | */ 35 | SmolRTSP_ReasonPhrase reason; 36 | } SmolRTSP_ResponseLine; 37 | 38 | /** 39 | * Serialises @p self into @p w. 40 | * 41 | * @param[in] self The instance to be serialised. 42 | * @param[in] w The writer to be provided with serialised data. 43 | * 44 | * @return The number of bytes written or a negative value on error. 45 | * 46 | * @pre `self != NULL` 47 | * @pre `w.self && w.vptr` 48 | */ 49 | ssize_t SmolRTSP_ResponseLine_serialize( 50 | const SmolRTSP_ResponseLine *restrict self, 51 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 52 | 53 | /** 54 | * Parses @p data to @p self. 55 | * 56 | * @pre `self != NULL` 57 | */ 58 | SmolRTSP_ParseResult SmolRTSP_ResponseLine_parse( 59 | SmolRTSP_ResponseLine *restrict self, 60 | CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 61 | 62 | /** 63 | * Tests @p lhs and @p rhs for equality. 64 | * 65 | * @pre `lhs != NULL` 66 | * @pre `rhs != NULL` 67 | */ 68 | bool SmolRTSP_ResponseLine_eq( 69 | const SmolRTSP_ResponseLine *restrict lhs, 70 | const SmolRTSP_ResponseLine *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 71 | -------------------------------------------------------------------------------- /tests/nal/h265.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | TEST parse(void) { 6 | const SmolRTSP_H265NalHeader h = 7 | SmolRTSP_H265NalHeader_parse((uint8_t[]){0b01001011, 0b10101101}); 8 | 9 | ASSERT(!h.forbidden_zero_bit); 10 | ASSERT_EQ(0b100101, h.unit_type); 11 | ASSERT_EQ(0b110101, h.nuh_layer_id); 12 | ASSERT_EQ(0b101, h.nuh_temporal_id_plus1); 13 | 14 | PASS(); 15 | } 16 | 17 | TEST serialize(void) { 18 | const SmolRTSP_H265NalHeader h = { 19 | .forbidden_zero_bit = false, 20 | .unit_type = 0b100101, 21 | .nuh_layer_id = 0b110101, 22 | .nuh_temporal_id_plus1 = 0b101, 23 | }; 24 | 25 | const uint16_t ret = SmolRTSP_H265NalHeader_serialize(h); 26 | ASSERT_EQ(0b1010110101001011, ret); 27 | 28 | PASS(); 29 | } 30 | 31 | TEST write_fu_header(void) { 32 | const SmolRTSP_H265NalHeader h = { 33 | .forbidden_zero_bit = false, 34 | .unit_type = 0b100101, 35 | .nuh_layer_id = 0b110101, 36 | .nuh_temporal_id_plus1 = 0b101, 37 | }; 38 | 39 | uint8_t buffer[SMOLRTSP_H265_FU_HEADER_SIZE] = {0}; 40 | 41 | #define CHECK(is_first_fragment, is_last_fragment, ...) \ 42 | do { \ 43 | SmolRTSP_H265NalHeader_write_fu_header( \ 44 | h, buffer, is_first_fragment, is_last_fragment); \ 45 | ASSERT_MEM_EQ(((uint8_t[]){__VA_ARGS__}), buffer, sizeof buffer); \ 46 | } while (0) 47 | 48 | CHECK(true, true, 0b01100011, 0b10101101, 0b11100101); 49 | CHECK(true, false, 0b01100011, 0b10101101, 0b10100101); 50 | CHECK(false, true, 0b01100011, 0b10101101, 0b01100101); 51 | CHECK(false, false, 0b01100011, 0b10101101, 0b00100101); 52 | 53 | #undef CHECK 54 | 55 | PASS(); 56 | } 57 | 58 | SUITE(nal_h265) { 59 | RUN_TEST(parse); 60 | RUN_TEST(serialize); 61 | RUN_TEST(write_fu_header); 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | compiler: [gcc, clang] 14 | 15 | runs-on: ubuntu-latest 16 | env: 17 | CC: ${{ matrix.compiler }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Install CMake and Clang 23 | run: sudo apt update && sudo apt install -y cmake 24 | 25 | - name: Install Clang 26 | if: matrix.compiler == 'clang' 27 | run: sudo apt install -y clang 28 | 29 | - name: Tests 30 | run: bash scripts/test.sh 31 | 32 | test-server: 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - name: Install libevent, CMake, FFMpeg 39 | run: sudo apt update && sudo apt install -y libevent-dev cmake ffmpeg xxd 40 | 41 | - name: Test the example server 42 | run: sudo bash scripts/test-server.sh 43 | 44 | check-fmt: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Install libevent 51 | run: sudo apt update && sudo apt install -y clang-format 52 | 53 | - name: Download run-clang-format 54 | run: git submodule update --init run-clang-format 55 | 56 | - name: Check code formatting 57 | run: bash scripts/check-fmt.sh 58 | 59 | deploy-docs: 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | 65 | - name: Install Doxygen 66 | run: sudo apt update && sudo apt install -y doxygen graphviz 67 | 68 | - name: Build the docs 69 | run: bash scripts/docs.sh 70 | 71 | - name: Deploy the docs 72 | if: ${{ !env.ACT && github.event_name != 'pull_request' }} 73 | uses: peaceiris/actions-gh-pages@v4 74 | with: 75 | github_token: ${{ secrets.GITHUB_TOKEN }} 76 | publish_dir: ./docs/ 77 | -------------------------------------------------------------------------------- /src/nal/h264.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | SmolRTSP_H264NalHeader SmolRTSP_H264NalHeader_parse(uint8_t byte_header) { 6 | return (SmolRTSP_H264NalHeader){ 7 | .forbidden_zero_bit = (byte_header & 0b10000000) >> 7 == 1, 8 | .ref_idc = (byte_header & 0b01100000) >> 5, 9 | .unit_type = (byte_header & 0b00011111), 10 | }; 11 | } 12 | 13 | uint8_t SmolRTSP_H264NalHeader_serialize(SmolRTSP_H264NalHeader self) { 14 | return (self.forbidden_zero_bit ? 0b10000000 : 0b00000000) | 15 | (self.ref_idc << 5) | (self.unit_type); 16 | } 17 | 18 | bool SmolRTSP_H264NalHeader_is_vps(SmolRTSP_H264NalHeader self) { 19 | (void)self; 20 | return false; 21 | } 22 | 23 | bool SmolRTSP_H264NalHeader_is_sps(SmolRTSP_H264NalHeader self) { 24 | return SMOLRTSP_H264_NAL_UNIT_SPS == self.unit_type; 25 | } 26 | 27 | bool SmolRTSP_H264NalHeader_is_pps(SmolRTSP_H264NalHeader self) { 28 | return SMOLRTSP_H264_NAL_UNIT_PPS == self.unit_type; 29 | } 30 | 31 | bool SmolRTSP_H264NalHeader_is_coded_slice_idr(SmolRTSP_H264NalHeader self) { 32 | return SMOLRTSP_H264_NAL_UNIT_CODED_SLICE_IDR == self.unit_type; 33 | } 34 | 35 | bool SmolRTSP_H264NalHeader_is_coded_slice_non_idr( 36 | SmolRTSP_H264NalHeader self) { 37 | return SMOLRTSP_H264_NAL_UNIT_CODED_SLICE_NON_IDR == self.unit_type; 38 | } 39 | 40 | void SmolRTSP_H264NalHeader_write_fu_header( 41 | SmolRTSP_H264NalHeader self, uint8_t buffer[restrict], 42 | bool is_first_fragment, bool is_last_fragment) { 43 | uint8_t fu_identifier = (uint8_t)0b01111100; // 0, nal_ref_idc, FU-A (28) 44 | 45 | if ((self.ref_idc & 0b00000010) == 0) { 46 | fu_identifier &= 0b00111111; 47 | } 48 | if ((self.ref_idc & 0b00000001) == 0) { 49 | fu_identifier &= 0b01011111; 50 | } 51 | 52 | const uint8_t fu_header = smolrtsp_nal_fu_header( 53 | is_first_fragment, is_last_fragment, self.unit_type); 54 | 55 | buffer = SLICE99_APPEND(buffer, fu_identifier); 56 | buffer = SLICE99_APPEND(buffer, fu_header); 57 | } 58 | -------------------------------------------------------------------------------- /src/types/rtsp_version.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "parsing.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | ssize_t SmolRTSP_RtspVersion_serialize( 13 | const SmolRTSP_RtspVersion *restrict self, SmolRTSP_Writer w) { 14 | assert(self); 15 | assert(w.self && w.vptr); 16 | 17 | return VCALL( 18 | w, writef, "RTSP/%" PRIu8 ".%" PRIu8, self->major, self->minor); 19 | } 20 | 21 | SmolRTSP_ParseResult SmolRTSP_RtspVersion_parse( 22 | SmolRTSP_RtspVersion *restrict self, CharSlice99 input) { 23 | assert(self); 24 | 25 | const CharSlice99 backup = input; 26 | 27 | MATCH(smolrtsp_match_whitespaces(input)); 28 | MATCH(smolrtsp_match_str(input, "RTSP/")); 29 | 30 | CharSlice99 major = input; 31 | MATCH(smolrtsp_match_numeric(input)); 32 | major = CharSlice99_from_ptrdiff(major.ptr, input.ptr); 33 | MATCH(smolrtsp_match_char(input, '.')); 34 | 35 | CharSlice99 minor = input; 36 | MATCH(smolrtsp_match_numeric(input)); 37 | minor = CharSlice99_from_ptrdiff(minor.ptr, input.ptr); 38 | 39 | uint8_t major_int, minor_int; 40 | 41 | if (sscanf(CharSlice99_alloca_c_str(major), "%" SCNu8, &major_int) != 1) { 42 | return SmolRTSP_ParseResult_Failure( 43 | SmolRTSP_ParseError_TypeMismatch(SmolRTSP_ParseType_Int, major)); 44 | } 45 | 46 | if (sscanf(CharSlice99_alloca_c_str(minor), "%" SCNu8, &minor_int) != 1) { 47 | return SmolRTSP_ParseResult_Failure( 48 | SmolRTSP_ParseError_TypeMismatch(SmolRTSP_ParseType_Int, minor)); 49 | } 50 | 51 | self->major = major_int; 52 | self->minor = minor_int; 53 | 54 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 55 | } 56 | 57 | bool SmolRTSP_RtspVersion_eq( 58 | const SmolRTSP_RtspVersion *restrict lhs, 59 | const SmolRTSP_RtspVersion *restrict rhs) { 60 | assert(lhs); 61 | assert(rhs); 62 | 63 | return lhs->major == rhs->major && lhs->minor == rhs->minor; 64 | } 65 | -------------------------------------------------------------------------------- /include/smolrtsp/types/method.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP method. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | /** 16 | * An RTSP method. 17 | */ 18 | typedef CharSlice99 SmolRTSP_Method; 19 | 20 | /** 21 | * Parses @p data to @p self. 22 | * 23 | * @pre `self != NULL` 24 | */ 25 | SmolRTSP_ParseResult SmolRTSP_Method_parse( 26 | SmolRTSP_Method *restrict self, CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 27 | 28 | /** 29 | * Tests @p lhs and @p rhs for equality. 30 | * 31 | * @pre `lhs != NULL` 32 | * @pre `rhs != NULL` 33 | */ 34 | bool SmolRTSP_Method_eq( 35 | const SmolRTSP_Method *restrict lhs, 36 | const SmolRTSP_Method *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 37 | 38 | /** 39 | * `OPTIONS`. 40 | */ 41 | #define SMOLRTSP_METHOD_OPTIONS (CharSlice99_from_str("OPTIONS")) 42 | 43 | /** 44 | * `DESCRIBE`. 45 | */ 46 | #define SMOLRTSP_METHOD_DESCRIBE (CharSlice99_from_str("DESCRIBE")) 47 | 48 | /** 49 | * `ANNOUNCE`. 50 | */ 51 | #define SMOLRTSP_METHOD_ANNOUNCE (CharSlice99_from_str("ANNOUNCE")) 52 | 53 | /** 54 | * `SETUP`. 55 | */ 56 | #define SMOLRTSP_METHOD_SETUP (CharSlice99_from_str("SETUP")) 57 | 58 | /** 59 | * `PLAY`. 60 | */ 61 | #define SMOLRTSP_METHOD_PLAY (CharSlice99_from_str("PLAY")) 62 | 63 | /** 64 | * `PAUSE`. 65 | */ 66 | #define SMOLRTSP_METHOD_PAUSE (CharSlice99_from_str("PAUSE")) 67 | 68 | /** 69 | * `TEARDOWN`. 70 | */ 71 | #define SMOLRTSP_METHOD_TEARDOWN (CharSlice99_from_str("TEARDOWN")) 72 | 73 | /** 74 | * `GET_PARAMETER`. 75 | */ 76 | #define SMOLRTSP_METHOD_GET_PARAMETER (CharSlice99_from_str("GET_PARAMETER")) 77 | 78 | /** 79 | * `SET_PARAMETER`. 80 | */ 81 | #define SMOLRTSP_METHOD_SET_PARAMETER (CharSlice99_from_str("SET_PARAMETER")) 82 | 83 | /** 84 | * `REDIRECT`. 85 | */ 86 | #define SMOLRTSP_METHOD_REDIRECT (CharSlice99_from_str("REDIRECT")) 87 | 88 | /** 89 | * `RECORD`. 90 | */ 91 | #define SMOLRTSP_METHOD_RECORD (CharSlice99_from_str("RECORD")) 92 | -------------------------------------------------------------------------------- /include/smolrtsp/types/request.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP request. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * An RTSP request. 20 | */ 21 | typedef struct { 22 | /** 23 | * The request line. 24 | */ 25 | SmolRTSP_RequestLine start_line; 26 | 27 | /** 28 | * The header map. 29 | */ 30 | SmolRTSP_HeaderMap header_map; 31 | 32 | /** 33 | * The message body. 34 | */ 35 | SmolRTSP_MessageBody body; 36 | 37 | /** 38 | * The sequence number for an RTSP request/response pair. 39 | */ 40 | uint32_t cseq; 41 | } SmolRTSP_Request; 42 | 43 | /** 44 | * Returns an RTSP request suitable for being parsed. 45 | */ 46 | SmolRTSP_Request SmolRTSP_Request_uninit(void) SMOLRTSP_PRIV_MUST_USE; 47 | 48 | /** 49 | * Serialises @p self into @p w. 50 | * 51 | * If `CSeq` and `Content-Length` are not present in 52 | * #SmolRTSP_Request.header_map, they will be taken from #SmolRTSP_Request.cseq 53 | * and #SmolRTSP_Request.body, respectively, and serialised as first headers 54 | * automatically. 55 | * 56 | * @param[in] self The instance to be serialised. 57 | * @param[in] w The writer to be provided with serialised data. 58 | * 59 | * @return The number of bytes written or a negative value on error. 60 | * 61 | * @pre `self != NULL` 62 | * @pre `w.self && w.vptr` 63 | */ 64 | ssize_t SmolRTSP_Request_serialize( 65 | const SmolRTSP_Request *restrict self, 66 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 67 | 68 | /** 69 | * Parses @p data to @p self. 70 | * 71 | * @pre `self != NULL` 72 | */ 73 | SmolRTSP_ParseResult SmolRTSP_Request_parse( 74 | SmolRTSP_Request *restrict self, CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 75 | 76 | /** 77 | * Tests @p lhs and @p rhs for equality. 78 | * 79 | * @pre `lhs != NULL` 80 | * @pre `rhs != NULL` 81 | */ 82 | bool SmolRTSP_Request_eq( 83 | const SmolRTSP_Request *restrict lhs, 84 | const SmolRTSP_Request *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 85 | -------------------------------------------------------------------------------- /include/smolrtsp/types/response.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP response. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * An RTSP response. 20 | */ 21 | typedef struct { 22 | /** 23 | * The response line. 24 | */ 25 | SmolRTSP_ResponseLine start_line; 26 | 27 | /** 28 | * The header map. 29 | */ 30 | SmolRTSP_HeaderMap header_map; 31 | 32 | /** 33 | * The message body. 34 | */ 35 | SmolRTSP_MessageBody body; 36 | 37 | /** 38 | * The sequence number for an RTSP request/response pair. 39 | */ 40 | uint32_t cseq; 41 | } SmolRTSP_Response; 42 | 43 | /** 44 | * Returns an RTSP response suitable for being parsed. 45 | */ 46 | SmolRTSP_Response SmolRTSP_Response_uninit(void) SMOLRTSP_PRIV_MUST_USE; 47 | 48 | /** 49 | * Serialises @p self into @p w. 50 | * 51 | * If `CSeq` and `Content-Length` are not present in 52 | * #SmolRTSP_Response.header_map, they will be taken from 53 | * #SmolRTSP_Response.cseq and #SmolRTSP_Response.body, respectively, and 54 | * serialised as first headers automatically. 55 | * 56 | * @param[in] self The instance to be serialised. 57 | * @param[in] w The writer to be provided with serialised data. 58 | * 59 | * @return The number of bytes written or a negative value on error. 60 | * 61 | * @pre `self != NULL` 62 | * @pre `w.self && w.vptr` 63 | */ 64 | ssize_t SmolRTSP_Response_serialize( 65 | const SmolRTSP_Response *restrict self, 66 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 67 | 68 | /** 69 | * Parses @p data to @p self. 70 | * 71 | * @pre `self != NULL` 72 | */ 73 | SmolRTSP_ParseResult SmolRTSP_Response_parse( 74 | SmolRTSP_Response *restrict self, CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 75 | 76 | /** 77 | * Tests @p lhs and @p rhs for equality. 78 | * 79 | * @pre `lhs != NULL` 80 | * @pre `rhs != NULL` 81 | */ 82 | bool SmolRTSP_Response_eq( 83 | const SmolRTSP_Response *restrict lhs, 84 | const SmolRTSP_Response *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 85 | -------------------------------------------------------------------------------- /src/transport/tcp.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | typedef struct { 15 | SmolRTSP_Writer w; 16 | int channel_id; 17 | size_t max_buffer; 18 | } SmolRTSP_TcpTransport; 19 | 20 | declImpl(SmolRTSP_Transport, SmolRTSP_TcpTransport); 21 | 22 | SmolRTSP_Transport smolrtsp_transport_tcp( 23 | SmolRTSP_Writer w, uint8_t channel_id, size_t max_buffer) { 24 | assert(w.self && w.vptr); 25 | 26 | SmolRTSP_TcpTransport *self = malloc(sizeof *self); 27 | assert(self); 28 | 29 | self->w = w; 30 | self->channel_id = channel_id; 31 | self->max_buffer = max_buffer; 32 | 33 | return DYN(SmolRTSP_TcpTransport, SmolRTSP_Transport, self); 34 | } 35 | 36 | static void SmolRTSP_TcpTransport_drop(VSelf) { 37 | VSELF(SmolRTSP_TcpTransport); 38 | assert(self); 39 | 40 | free(self); 41 | } 42 | 43 | impl(SmolRTSP_Droppable, SmolRTSP_TcpTransport); 44 | 45 | static int SmolRTSP_TcpTransport_transmit(VSelf, SmolRTSP_IoVecSlice bufs) { 46 | VSELF(SmolRTSP_TcpTransport); 47 | assert(self); 48 | 49 | const size_t total_bytes = SmolRTSP_IoVecSlice_len(bufs); 50 | 51 | const uint32_t header = 52 | smolrtsp_interleaved_header(self->channel_id, htons(total_bytes)); 53 | 54 | VCALL(self->w, lock); 55 | ssize_t ret = 56 | VCALL(self->w, write, CharSlice99_new((char *)&header, sizeof header)); 57 | if (ret != sizeof header) { 58 | VCALL(self->w, unlock); 59 | return -1; 60 | } 61 | 62 | for (size_t i = 0; i < bufs.len; i++) { 63 | const CharSlice99 vec = 64 | CharSlice99_new(bufs.ptr[i].iov_base, bufs.ptr[i].iov_len); 65 | ret = VCALL(self->w, write, vec); 66 | if (ret != (ssize_t)vec.len) { 67 | VCALL(self->w, unlock); 68 | return -1; 69 | } 70 | } 71 | VCALL(self->w, unlock); 72 | 73 | return 0; 74 | } 75 | 76 | static bool SmolRTSP_TcpTransport_is_full(VSelf) { 77 | VSELF(SmolRTSP_TcpTransport); 78 | assert(self); 79 | 80 | return VCALL(self->w, filled) > self->max_buffer; 81 | } 82 | 83 | impl(SmolRTSP_Transport, SmolRTSP_TcpTransport); 84 | -------------------------------------------------------------------------------- /tests/types/sdp.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #define AUDIO_PORT 123 6 | #define AUDIO_RTP_PAYLOAD_TYPE 456 7 | #define SERVER_IP_ADDR "0.0.0.0" 8 | 9 | TEST serialize_sdp_line(void) { 10 | char buffer[20] = {0}; 11 | 12 | const SmolRTSP_SdpLine sdp = { 13 | .ty = SMOLRTSP_SDP_ATTR, 14 | .value = CharSlice99_from_str("abc"), 15 | }; 16 | 17 | const ssize_t ret = 18 | SmolRTSP_SdpLine_serialize(&sdp, smolrtsp_string_writer(buffer)); 19 | 20 | const char *expected = "a=abc\r\n"; 21 | 22 | ASSERT_EQ((ssize_t)strlen(expected), ret); 23 | ASSERT_STR_EQ(expected, buffer); 24 | 25 | PASS(); 26 | } 27 | 28 | TEST sdp_printf(void) { 29 | char buffer[20] = {0}; 30 | 31 | const char *expected = "a=abc 123 @ def\r\n"; 32 | 33 | const ssize_t ret = smolrtsp_sdp_printf( 34 | smolrtsp_string_writer(buffer), SMOLRTSP_SDP_ATTR, "abc %d @ %s", 123, 35 | "def"); 36 | ASSERT_EQ((ssize_t)strlen(expected), ret); 37 | ASSERT_STR_EQ(expected, buffer); 38 | 39 | PASS(); 40 | } 41 | 42 | TEST sdp_describe(void) { 43 | char buffer[256] = {0}; 44 | const SmolRTSP_Writer sdp = smolrtsp_string_writer(buffer); 45 | ssize_t ret = 0; 46 | 47 | // clang-format off 48 | SMOLRTSP_SDP_DESCRIBE( 49 | ret, sdp, 50 | (SMOLRTSP_SDP_VERSION, "0"), 51 | (SMOLRTSP_SDP_ORIGIN, "SmolRTSP 3855320066 3855320129 IN IP4 0.0.0.0"), 52 | (SMOLRTSP_SDP_SESSION_NAME, "SmolRTSP test"), 53 | (SMOLRTSP_SDP_CONNECTION, "IN IP4 %s", SERVER_IP_ADDR), 54 | (SMOLRTSP_SDP_TIME, "0 0"), 55 | (SMOLRTSP_SDP_MEDIA, "audio %d RTP/AVP %d", AUDIO_PORT, AUDIO_RTP_PAYLOAD_TYPE), 56 | (SMOLRTSP_SDP_ATTR, "control:audio")); 57 | // clang-format on 58 | 59 | const char *expected = 60 | "v=0\r\n" 61 | "o=SmolRTSP 3855320066 3855320129 IN IP4 0.0.0.0\r\n" 62 | "s=SmolRTSP test\r\n" 63 | "c=IN IP4 0.0.0.0\r\n" 64 | "t=0 0\r\n" 65 | "m=audio 123 RTP/AVP 456\r\n" 66 | "a=control:audio\r\n"; 67 | 68 | ASSERT_EQ((ssize_t)strlen(expected), ret); 69 | ASSERT_STR_EQ(expected, buffer); 70 | 71 | PASS(); 72 | } 73 | 74 | SUITE(types_sdp) { 75 | RUN_TEST(serialize_sdp_line); 76 | RUN_TEST(sdp_printf); 77 | RUN_TEST(sdp_describe); 78 | } 79 | -------------------------------------------------------------------------------- /src/types/parsing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #define MATCH(parse_expr) \ 13 | do { \ 14 | const SmolRTSP_ParseResult parse_res_var = parse_expr; \ 15 | \ 16 | match(parse_res_var) { \ 17 | of(SmolRTSP_ParseResult_Success, status) { \ 18 | match(*status) { \ 19 | of(SmolRTSP_ParseStatus_Complete, offset) input = \ 20 | CharSlice99_advance(input, *offset); \ 21 | otherwise return parse_res_var; \ 22 | } \ 23 | } \ 24 | of(SmolRTSP_ParseResult_Failure, err) { \ 25 | return SmolRTSP_ParseResult_Failure(*err); \ 26 | } \ 27 | } \ 28 | } while (0) 29 | 30 | /** 31 | * Consume @p input until @p matcher returns false. 32 | */ 33 | SmolRTSP_ParseResult smolrtsp_match_until( 34 | CharSlice99 input, bool (*matcher)(char c, void *ctx), void *ctx); 35 | 36 | SmolRTSP_ParseResult 37 | smolrtsp_match_until_str(CharSlice99 input, const char *restrict str); 38 | 39 | SmolRTSP_ParseResult smolrtsp_match_until_crlf(CharSlice99 input); 40 | SmolRTSP_ParseResult smolrtsp_match_until_double_crlf(CharSlice99 input); 41 | SmolRTSP_ParseResult smolrtsp_match_char(CharSlice99 input, char c); 42 | SmolRTSP_ParseResult 43 | smolrtsp_match_str(CharSlice99 input, const char *restrict str); 44 | SmolRTSP_ParseResult smolrtsp_match_whitespaces(CharSlice99 input); 45 | SmolRTSP_ParseResult smolrtsp_match_non_whitespaces(CharSlice99 input); 46 | SmolRTSP_ParseResult smolrtsp_match_numeric(CharSlice99 input); 47 | SmolRTSP_ParseResult smolrtsp_match_ident(CharSlice99 input); 48 | SmolRTSP_ParseResult smolrtsp_match_header_name(CharSlice99 input); 49 | -------------------------------------------------------------------------------- /tests/types/request.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define TEST_PARSE_INIT_TYPE(result) result = SmolRTSP_Request_uninit() 4 | 5 | #include "test_util.h" 6 | #include 7 | 8 | DEF_TEST_PARSE(SmolRTSP_Request) 9 | 10 | TEST parse_request(void) { 11 | const SmolRTSP_Request expected = { 12 | .start_line = 13 | { 14 | .method = SMOLRTSP_METHOD_DESCRIBE, 15 | .uri = CharSlice99_from_str("http://example.com"), 16 | .version = {.major = 1, .minor = 1}, 17 | }, 18 | .header_map = SmolRTSP_HeaderMap_from_array({ 19 | {SMOLRTSP_HEADER_C_SEQ, CharSlice99_from_str("123")}, 20 | {SMOLRTSP_HEADER_CONTENT_LENGTH, CharSlice99_from_str("10")}, 21 | {SMOLRTSP_HEADER_ACCEPT_LANGUAGE, CharSlice99_from_str("English")}, 22 | {SMOLRTSP_HEADER_CONTENT_TYPE, 23 | CharSlice99_from_str("application/octet-stream")}, 24 | }), 25 | .body = CharSlice99_from_str("0123456789"), 26 | .cseq = 123, 27 | }; 28 | 29 | TEST_PARSE( 30 | "DESCRIBE http://example.com RTSP/1.1\r\n" 31 | "CSeq: 123\r\n" 32 | "Content-Length: 10\r\n" 33 | "Accept-Language: English\r\n" 34 | "Content-Type: application/octet-stream\r\n" 35 | "\r\n0123456789", 36 | expected); 37 | 38 | PASS(); 39 | } 40 | 41 | TEST serialize_request(void) { 42 | char buffer[500] = {0}; 43 | 44 | const SmolRTSP_Request request = { 45 | .start_line = 46 | { 47 | .method = SMOLRTSP_METHOD_DESCRIBE, 48 | .uri = CharSlice99_from_str("http://example.com"), 49 | .version = {1, 0}, 50 | }, 51 | .header_map = SmolRTSP_HeaderMap_from_array({ 52 | {SMOLRTSP_HEADER_CONTENT_TYPE, 53 | CharSlice99_from_str("application/octet-stream")}, 54 | }), 55 | .body = CharSlice99_from_str("1234567890"), 56 | .cseq = 456, 57 | }; 58 | 59 | const ssize_t ret = 60 | SmolRTSP_Request_serialize(&request, smolrtsp_string_writer(buffer)); 61 | 62 | const char *expected = 63 | "DESCRIBE http://example.com RTSP/1.0\r\n" 64 | "CSeq: 456\r\n" 65 | "Content-Length: 10\r\n" 66 | "Content-Type: application/octet-stream\r\n" 67 | "\r\n1234567890"; 68 | 69 | ASSERT_EQ((ssize_t)strlen(expected), ret); 70 | ASSERT_STR_EQ(expected, buffer); 71 | 72 | PASS(); 73 | } 74 | 75 | SUITE(types_request) { 76 | RUN_TEST(parse_request); 77 | RUN_TEST(serialize_request); 78 | } 79 | -------------------------------------------------------------------------------- /src/nal/h265.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | SmolRTSP_H265NalHeader 9 | SmolRTSP_H265NalHeader_parse(uint8_t bytes[restrict static 2]) { 10 | return (SmolRTSP_H265NalHeader){ 11 | .forbidden_zero_bit = (bytes[0] & 0b10000000) >> 7, 12 | .unit_type = (bytes[0] & 0b01111110) >> 1, 13 | .nuh_layer_id = 14 | ((bytes[0] & 0b00000001) << 5) | ((bytes[1] & 0b11111000) >> 3), 15 | .nuh_temporal_id_plus1 = (bytes[1] & 0b00000111), 16 | }; 17 | } 18 | 19 | uint16_t SmolRTSP_H265NalHeader_serialize(SmolRTSP_H265NalHeader self) { 20 | const uint8_t bytes[2] = { 21 | [0] = (((uint8_t)self.forbidden_zero_bit & 0b00000001) << 7) | 22 | ((self.unit_type & 0b00111111) << 1) | 23 | ((self.nuh_layer_id & 0b00100000) >> 5), 24 | 25 | [1] = ((self.nuh_layer_id & 0b00011111) << 3) | 26 | (self.nuh_temporal_id_plus1 & 0b00000111), 27 | }; 28 | 29 | uint16_t n; 30 | memcpy(&n, bytes, sizeof bytes); 31 | return n; 32 | } 33 | 34 | bool SmolRTSP_H265NalHeader_is_vps(SmolRTSP_H265NalHeader self) { 35 | return SMOLRTSP_H265_NAL_UNIT_VPS_NUT == self.unit_type; 36 | } 37 | 38 | bool SmolRTSP_H265NalHeader_is_sps(SmolRTSP_H265NalHeader self) { 39 | return SMOLRTSP_H265_NAL_UNIT_SPS_NUT == self.unit_type; 40 | } 41 | 42 | bool SmolRTSP_H265NalHeader_is_pps(SmolRTSP_H265NalHeader self) { 43 | return SMOLRTSP_H265_NAL_UNIT_PPS_NUT == self.unit_type; 44 | } 45 | 46 | bool SmolRTSP_H265NalHeader_is_coded_slice_idr(SmolRTSP_H265NalHeader self) { 47 | return SMOLRTSP_H265_NAL_UNIT_IDR_W_RADL == self.unit_type; 48 | } 49 | 50 | bool SmolRTSP_H265NalHeader_is_coded_slice_non_idr( 51 | SmolRTSP_H265NalHeader self) { 52 | return SMOLRTSP_H265_NAL_UNIT_IDR_N_LP == self.unit_type || 53 | SMOLRTSP_H265_NAL_UNIT_TRAIL_R == self.unit_type; 54 | } 55 | 56 | void SmolRTSP_H265NalHeader_write_fu_header( 57 | SmolRTSP_H265NalHeader self, uint8_t buffer[restrict], 58 | bool is_first_fragment, bool is_last_fragment) { 59 | const uint16_t payload_hdr = 60 | SmolRTSP_H265NalHeader_serialize((SmolRTSP_H265NalHeader){ 61 | .forbidden_zero_bit = self.forbidden_zero_bit, 62 | .unit_type = 49, 63 | .nuh_layer_id = self.nuh_layer_id, 64 | .nuh_temporal_id_plus1 = self.nuh_temporal_id_plus1, 65 | }); 66 | 67 | const uint8_t fu_header = smolrtsp_nal_fu_header( 68 | is_first_fragment, is_last_fragment, self.unit_type); 69 | 70 | buffer = SLICE99_APPEND(buffer, payload_hdr); 71 | buffer = SLICE99_APPEND(buffer, fu_header); 72 | } 73 | -------------------------------------------------------------------------------- /tests/types/response.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define TEST_PARSE_INIT_TYPE(result) result = SmolRTSP_Response_uninit() 4 | 5 | #include "test_util.h" 6 | #include 7 | 8 | DEF_TEST_PARSE(SmolRTSP_Response) 9 | 10 | TEST parse_response(void) { 11 | const SmolRTSP_Response expected = { 12 | .start_line = 13 | { 14 | .version = {.major = 1, .minor = 1}, 15 | .code = SMOLRTSP_STATUS_OK, 16 | .reason = CharSlice99_from_str("OK"), 17 | }, 18 | .header_map = SmolRTSP_HeaderMap_from_array({ 19 | {SMOLRTSP_HEADER_C_SEQ, CharSlice99_from_str("123")}, 20 | {SMOLRTSP_HEADER_CONTENT_LENGTH, CharSlice99_from_str("10")}, 21 | {SMOLRTSP_HEADER_ACCEPT_LANGUAGE, CharSlice99_from_str("English")}, 22 | {SMOLRTSP_HEADER_CONTENT_TYPE, 23 | CharSlice99_from_str("application/octet-stream")}, 24 | }), 25 | .body = CharSlice99_from_str("0123456789"), 26 | .cseq = 123, 27 | }; 28 | 29 | TEST_PARSE( 30 | "RTSP/1.1 200 OK\r\n" 31 | "CSeq: 123\r\n" 32 | "Content-Length: 10\r\n" 33 | "Accept-Language: English\r\n" 34 | "Content-Type: application/octet-stream\r\n" 35 | "\r\n0123456789", 36 | expected); 37 | 38 | PASS(); 39 | } 40 | 41 | TEST serialize_response(void) { 42 | char buffer[500] = {0}; 43 | 44 | const SmolRTSP_Response response = { 45 | .start_line = 46 | { 47 | .version = {1, 0}, 48 | .code = SMOLRTSP_STATUS_OK, 49 | .reason = CharSlice99_from_str("OK"), 50 | }, 51 | .header_map = SmolRTSP_HeaderMap_from_array({ 52 | {SMOLRTSP_HEADER_DATE, 53 | CharSlice99_from_str("Thu, 05 Jun 1997 18:57:19 GMT")}, 54 | {SMOLRTSP_HEADER_CONTENT_TYPE, 55 | CharSlice99_from_str("application/octet-stream")}, 56 | }), 57 | .body = CharSlice99_from_str("1234567890"), 58 | .cseq = 456, 59 | }; 60 | 61 | const ssize_t ret = 62 | SmolRTSP_Response_serialize(&response, smolrtsp_string_writer(buffer)); 63 | 64 | const char *expected = 65 | "RTSP/1.0 200 OK\r\n" 66 | "CSeq: 456\r\n" 67 | "Content-Length: 10\r\n" 68 | "Date: Thu, 05 Jun 1997 18:57:19 GMT\r\n" 69 | "Content-Type: application/octet-stream\r\n" 70 | "\r\n1234567890"; 71 | 72 | ASSERT_EQ((ssize_t)strlen(expected), ret); 73 | ASSERT_STR_EQ(expected, buffer); 74 | 75 | PASS(); 76 | } 77 | 78 | SUITE(types_response) { 79 | RUN_TEST(parse_response); 80 | RUN_TEST(serialize_response); 81 | } 82 | -------------------------------------------------------------------------------- /include/smolrtsp/rtp_transport.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTP data transport. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | /** 20 | * An RTP data transport. 21 | */ 22 | typedef struct SmolRTSP_RtpTransport SmolRTSP_RtpTransport; 23 | 24 | /** 25 | * An RTP timestamp. 26 | * 27 | * ## Variants 28 | * 29 | * - `Raw` -- The value to be assigned to #SmolRTSP_RtpHeader.timestamp without 30 | * further conversion. 31 | * - `SysClockUs` -- The timestamp value in microseconds derived from a system 32 | * clock (e.g., `clock_gettime`). It should be used when a raw timestamp cannot 33 | * be computed, as typically occurs with real-time video. 34 | * 35 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 36 | */ 37 | 38 | // clang-format off 39 | datatype99( 40 | SmolRTSP_RtpTimestamp, 41 | (SmolRTSP_RtpTimestamp_Raw, uint32_t), 42 | (SmolRTSP_RtpTimestamp_SysClockUs, uint64_t) 43 | ); 44 | // clang-format on 45 | 46 | /** 47 | * Creates a new RTP transport from the underlying level-4 protocol @p t. 48 | * 49 | * @param[in] t The level-4 protocol (such as TCP or UDP). 50 | * @param[in] payload_ty The RTP payload type. The list of payload types is 51 | * available here: . 52 | * @param[in] clock_rate The RTP clock rate of @p payload_ty (HZ). 53 | * 54 | * @pre `t.self && t.vptr` 55 | * @pre The `rand` PRNG must be set up via `srand`. 56 | */ 57 | SmolRTSP_RtpTransport *SmolRTSP_RtpTransport_new( 58 | SmolRTSP_Transport t, uint8_t payload_ty, 59 | uint32_t clock_rate) SMOLRTSP_PRIV_MUST_USE; 60 | 61 | /** 62 | * Sends an RTP packet. 63 | * 64 | * @param[out] self The RTP transport for sending this packet. 65 | * @param[in] ts The RTP timestamp for this packet. 66 | * @param[in] marker The RTP marker flag. 67 | * @param[in] payload_header The payload header. Can be `U8Slice99_empty()`. 68 | * @param[in] payload The payload data. 69 | * 70 | * @pre `self != NULL` 71 | * 72 | * @return -1 if an I/O error occurred and sets `errno` appropriately, 0 on 73 | * success. 74 | */ 75 | int SmolRTSP_RtpTransport_send_packet( 76 | SmolRTSP_RtpTransport *self, SmolRTSP_RtpTimestamp ts, bool marker, 77 | U8Slice99 payload_header, U8Slice99 payload) SMOLRTSP_PRIV_MUST_USE; 78 | 79 | /** 80 | * Implements #SmolRTSP_Droppable_IFACE for #SmolRTSP_RtpTransport. 81 | * 82 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 83 | * usage. 84 | */ 85 | declImplExtern99(SmolRTSP_Droppable, SmolRTSP_RtpTransport); 86 | 87 | bool SmolRTSP_RtpTransport_is_full(SmolRTSP_RtpTransport *self); 88 | -------------------------------------------------------------------------------- /tests/types/test_util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #define TEST_PARSE(input, expected) CHECK_CALL(test_parse(input, expected)) 11 | 12 | #ifndef TEST_PARSE_INIT_TYPE 13 | #define TEST_PARSE_INIT_TYPE(_result) 14 | #endif 15 | 16 | #ifndef TEST_PARSE_DEINIT_TYPE 17 | #define TEST_PARSE_DEINIT_TYPE(_result) 18 | #endif 19 | 20 | #define DEF_TEST_PARSE(T) \ 21 | static enum greatest_test_res test_parse(const char *str, T expected) { \ 22 | T result; \ 23 | TEST_PARSE_INIT_TYPE(result); \ 24 | const CharSlice99 input = CharSlice99_from_str((char *)str); \ 25 | SmolRTSP_ParseResult ret; \ 26 | \ 27 | for (size_t i = 1; i < input.len; i++) { \ 28 | ret = T##_parse(&result, CharSlice99_sub(input, 0, i)); \ 29 | ASSERT(SmolRTSP_ParseResult_is_partial(ret)); \ 30 | } \ 31 | \ 32 | ret = T##_parse(&result, input); \ 33 | match(ret) { \ 34 | of(SmolRTSP_ParseResult_Success, status) { \ 35 | ASSERT(SmolRTSP_ParseStatus_is_complete(*status)); \ 36 | /* ASSERT_EQ(input.len, status->offset); */ \ 37 | ASSERT(T##_eq(&result, &expected)); \ 38 | } \ 39 | of(SmolRTSP_ParseResult_Failure, error) { \ 40 | const int err_bytes = SmolRTSP_ParseError_print( \ 41 | *error, smolrtsp_file_writer(stderr)); \ 42 | ASSERT(err_bytes >= 0); \ 43 | FAILm("Parsing failed"); \ 44 | } \ 45 | } \ 46 | \ 47 | TEST_PARSE_DEINIT_TYPE(result); \ 48 | PASS(); \ 49 | } 50 | -------------------------------------------------------------------------------- /tests/transport.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #define DATA_0 "abc" 14 | #define DATA_1 "defghi" 15 | 16 | static enum greatest_test_res test_transport( 17 | SmolRTSP_Transport t, int read_fd, size_t len, 18 | const char expected[restrict static len]) { 19 | struct iovec bufs[] = { 20 | {.iov_base = DATA_0, .iov_len = sizeof((char[]){DATA_0}) - 1}, 21 | {.iov_base = DATA_1, .iov_len = sizeof((char[]){DATA_1}) - 1}, 22 | }; 23 | 24 | const SmolRTSP_IoVecSlice slices = Slice99_typed_from_array(bufs); 25 | 26 | const ssize_t ret = VCALL(t, transmit, slices); 27 | ASSERT_EQ(0, ret); 28 | 29 | char *buffer = malloc(len); 30 | read(read_fd, buffer, len); 31 | ASSERT_MEM_EQ(expected, buffer, len); 32 | free(buffer); 33 | 34 | VCALL_SUPER(t, SmolRTSP_Droppable, drop); 35 | 36 | PASS(); 37 | } 38 | 39 | TEST check_tcp(void) { 40 | int fds[2]; 41 | const bool socketpair_ok = socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0; 42 | ASSERT(socketpair_ok); 43 | 44 | const uint8_t chn_id = 123; 45 | 46 | SmolRTSP_Transport tcp = 47 | smolrtsp_transport_tcp(smolrtsp_fd_writer(&fds[0]), chn_id, 0); 48 | 49 | const char total_len = strlen(DATA_0) + strlen(DATA_1); 50 | const char expected[] = {'$', chn_id, 0, total_len, 'a', 'b', 'c', 51 | 'd', 'e', 'f', 'g', 'h', 'i'}; 52 | 53 | CHECK_CALL(test_transport(tcp, fds[1], sizeof expected, expected)); 54 | 55 | close(fds[0]); 56 | close(fds[1]); 57 | PASS(); 58 | } 59 | 60 | TEST check_udp(void) { 61 | int fds[2]; 62 | const bool socketpair_ok = socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds) == 0; 63 | ASSERT(socketpair_ok); 64 | 65 | SmolRTSP_Transport udp = smolrtsp_transport_udp(fds[0]); 66 | 67 | char expected[] = {DATA_0 DATA_1}; 68 | 69 | CHECK_CALL(test_transport(udp, fds[1], strlen(expected), expected)); 70 | 71 | close(fds[0]); 72 | close(fds[1]); 73 | PASS(); 74 | } 75 | 76 | TEST sockaddr_get_ipv4(void) { 77 | struct sockaddr_storage addr; 78 | memset(&addr, '\0', sizeof addr); 79 | addr.ss_family = AF_INET; 80 | 81 | ASSERT_EQ( 82 | (void *)&((struct sockaddr_in *)&addr)->sin_addr, 83 | smolrtsp_sockaddr_ip((const struct sockaddr *)&addr)); 84 | 85 | PASS(); 86 | } 87 | 88 | TEST sockaddr_get_ipv6(void) { 89 | struct sockaddr_storage addr; 90 | memset(&addr, '\0', sizeof addr); 91 | addr.ss_family = AF_INET6; 92 | 93 | ASSERT_EQ( 94 | (void *)&((struct sockaddr_in6 *)&addr)->sin6_addr, 95 | smolrtsp_sockaddr_ip((const struct sockaddr *)&addr)); 96 | 97 | PASS(); 98 | } 99 | 100 | TEST sockaddr_get_unknown(void) { 101 | struct sockaddr_storage addr; 102 | memset(&addr, '\0', sizeof addr); 103 | addr.ss_family = AF_UNIX; 104 | 105 | ASSERT_EQ(NULL, smolrtsp_sockaddr_ip((const struct sockaddr *)&addr)); 106 | 107 | PASS(); 108 | } 109 | 110 | SUITE(transport) { 111 | RUN_TEST(check_tcp); 112 | RUN_TEST(check_udp); 113 | RUN_TEST(sockaddr_get_ipv4); 114 | RUN_TEST(sockaddr_get_ipv6); 115 | RUN_TEST(sockaddr_get_unknown); 116 | } 117 | -------------------------------------------------------------------------------- /src/rtp_transport.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | struct SmolRTSP_RtpTransport { 12 | uint16_t seq_num; 13 | uint32_t ssrc; 14 | uint8_t payload_ty; 15 | uint32_t clock_rate; 16 | SmolRTSP_Transport transport; 17 | }; 18 | 19 | static uint32_t 20 | compute_timestamp(SmolRTSP_RtpTimestamp ts, uint32_t clock_rate); 21 | 22 | SmolRTSP_RtpTransport *SmolRTSP_RtpTransport_new( 23 | SmolRTSP_Transport t, uint8_t payload_ty, uint32_t clock_rate) { 24 | assert(t.self && t.vptr); 25 | 26 | SmolRTSP_RtpTransport *self = malloc(sizeof *self); 27 | assert(self); 28 | 29 | self->seq_num = 0; 30 | self->ssrc = (uint32_t)rand(); 31 | self->payload_ty = payload_ty; 32 | self->clock_rate = clock_rate; 33 | self->transport = t; 34 | 35 | return self; 36 | } 37 | 38 | static void SmolRTSP_RtpTransport_drop(VSelf) { 39 | VSELF(SmolRTSP_RtpTransport); 40 | assert(self); 41 | 42 | VCALL_SUPER(self->transport, SmolRTSP_Droppable, drop); 43 | 44 | free(self); 45 | } 46 | 47 | implExtern(SmolRTSP_Droppable, SmolRTSP_RtpTransport); 48 | 49 | int SmolRTSP_RtpTransport_send_packet( 50 | SmolRTSP_RtpTransport *self, SmolRTSP_RtpTimestamp ts, bool marker, 51 | U8Slice99 payload_header, U8Slice99 payload) { 52 | assert(self); 53 | 54 | const SmolRTSP_RtpHeader header = { 55 | .version = 2, 56 | .padding = false, 57 | .extension = false, 58 | .csrc_count = 0, 59 | .marker = marker, 60 | .payload_ty = self->payload_ty, 61 | .sequence_number = htons(self->seq_num), 62 | .timestamp = htobe32(compute_timestamp(ts, self->clock_rate)), 63 | .ssrc = self->ssrc, 64 | .csrc = NULL, 65 | .extension_profile = htons(0), 66 | .extension_payload_len = htons(0), 67 | .extension_payload = NULL, 68 | }; 69 | 70 | const size_t rtp_header_size = SmolRTSP_RtpHeader_size(header); 71 | const U8Slice99 rtp_header = U8Slice99_new( 72 | SmolRTSP_RtpHeader_serialize(header, alloca(rtp_header_size)), 73 | rtp_header_size); 74 | 75 | const SmolRTSP_IoVecSlice bufs = 76 | (SmolRTSP_IoVecSlice)Slice99_typed_from_array((struct iovec[]){ 77 | smolrtsp_slice_to_iovec(rtp_header), 78 | smolrtsp_slice_to_iovec(payload_header), 79 | smolrtsp_slice_to_iovec(payload), 80 | }); 81 | 82 | const int ret = VCALL(self->transport, transmit, bufs); 83 | if (ret != -1) { 84 | self->seq_num++; 85 | } 86 | 87 | return ret; 88 | } 89 | 90 | static uint32_t 91 | compute_timestamp(SmolRTSP_RtpTimestamp ts, uint32_t clock_rate) { 92 | match(ts) { 93 | of(SmolRTSP_RtpTimestamp_Raw, raw_ts) { 94 | return *raw_ts; 95 | } 96 | of(SmolRTSP_RtpTimestamp_SysClockUs, time_us) { 97 | const uint64_t us_rem = *time_us % 1000, 98 | ms = (*time_us - us_rem) / 1000; 99 | uint32_t clock_rate_kHz = clock_rate / 1000; 100 | return ms * clock_rate_kHz + 101 | (uint32_t)(us_rem * ((double)clock_rate_kHz / 1000.0)); 102 | } 103 | } 104 | 105 | return 0; 106 | } 107 | 108 | bool SmolRTSP_RtpTransport_is_full(SmolRTSP_RtpTransport *self) { 109 | return VCALL(self->transport, is_full); 110 | } 111 | -------------------------------------------------------------------------------- /tests/writer.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | TEST fd_writer(void) { 11 | int fds[2]; 12 | const bool socketpair_ok = socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0; 13 | ASSERT(socketpair_ok); 14 | 15 | SmolRTSP_Writer w = smolrtsp_fd_writer(&fds[0]); 16 | 17 | char buffer[32] = {0}; 18 | 19 | { 20 | ssize_t ret = VCALL(w, write, CharSlice99_from_str("abc")); 21 | ASSERT_EQ(3, ret); 22 | read(fds[1], buffer, ret); 23 | ASSERT_STRN_EQ("abc", buffer, ret); 24 | } 25 | 26 | { 27 | ssize_t ret = VCALL(w, writef, "%d abc %s", 123, "!@#"); 28 | ASSERT_EQ(11, ret); 29 | read(fds[1], buffer, ret); 30 | ASSERT_STRN_EQ("123 abc !@#", buffer, ret); 31 | } 32 | 33 | close(fds[0]); 34 | close(fds[1]); 35 | 36 | PASS(); 37 | } 38 | 39 | TEST file_writer(void) { 40 | FILE *tmp = tmpfile(); 41 | assert(tmp); 42 | 43 | SmolRTSP_Writer w = smolrtsp_file_writer(tmp); 44 | 45 | char buffer[32] = {0}; 46 | 47 | { 48 | ssize_t ret = VCALL(w, write, CharSlice99_from_str("abc")); 49 | ASSERT_EQ(3, ret); 50 | fseek(tmp, 0, SEEK_SET); 51 | fread(buffer, sizeof(char), ret, tmp); 52 | ASSERT_STRN_EQ("abc", buffer, ret); 53 | } 54 | 55 | { 56 | ssize_t ret = VCALL(w, writef, "%d abc %s", 123, "!@#"); 57 | ASSERT_EQ(11, ret); 58 | fseek(tmp, 0, SEEK_SET); 59 | fread(buffer, sizeof(char), ret, tmp); 60 | ASSERT_STRN_EQ("abc123 abc !@#", buffer, ret); 61 | } 62 | 63 | fclose(tmp); 64 | 65 | PASS(); 66 | } 67 | 68 | TEST string_writer(void) { 69 | char buffer[32] = {0}; 70 | 71 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 72 | 73 | { 74 | ssize_t ret = VCALL(w, write, CharSlice99_from_str("abc")); 75 | ASSERT_EQ(3, ret); 76 | ASSERT_STRN_EQ("abc", buffer, ret); 77 | } 78 | 79 | { 80 | ssize_t ret = VCALL(w, writef, "%d abc %s", 123, "!@#"); 81 | ASSERT_EQ(11, ret); 82 | ASSERT_STR_EQ("abc123 abc !@#", buffer); 83 | } 84 | 85 | PASS(); 86 | } 87 | 88 | TEST write_slices(void) { 89 | char buffer[128] = {0}; 90 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 91 | 92 | const CharSlice99 slices[] = { 93 | CharSlice99_from_str("abc"), 94 | CharSlice99_from_str("~"), 95 | CharSlice99_from_str("&* 123"), 96 | }; 97 | 98 | const ssize_t bytes_written = 99 | smolrtsp_write_slices(w, SLICE99_ARRAY_LEN(slices), slices); 100 | ASSERT_EQ(10, bytes_written); 101 | ASSERT_STR_EQ("abc~&* 123", buffer); 102 | 103 | PASS(); 104 | } 105 | 106 | TEST write_slices_macro(void) { 107 | char buffer[128] = {0}; 108 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 109 | 110 | const ssize_t bytes_written = SMOLRTSP_WRITE_SLICES( 111 | w, { 112 | CharSlice99_from_str("abc"), 113 | CharSlice99_from_str("~"), 114 | CharSlice99_from_str("&* 123"), 115 | }); 116 | ASSERT_EQ(10, bytes_written); 117 | ASSERT_STR_EQ("abc~&* 123", buffer); 118 | 119 | PASS(); 120 | } 121 | 122 | SUITE(writer) { 123 | RUN_TEST(fd_writer); 124 | RUN_TEST(file_writer); 125 | RUN_TEST(string_writer); 126 | 127 | RUN_TEST(write_slices); 128 | RUN_TEST(write_slices_macro); 129 | } 130 | -------------------------------------------------------------------------------- /include/smolrtsp/nal_transport.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTP/NAL data transport. 4 | * 5 | * @see RTP Payload Format for H.264 Video: 6 | * 7 | * @see RTP Payload Format for High Efficiency Video Coding (HEVC): 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | /** 23 | * The default value for #SmolRTSP_NalTransportConfig.max_h264_nalu_size. 24 | */ 25 | #define SMOLRTSP_MAX_H264_NALU_SIZE 1200 26 | 27 | /** 28 | * The default value for #SmolRTSP_NalTransportConfig.max_h265_nalu_size. 29 | */ 30 | #define SMOLRTSP_MAX_H265_NALU_SIZE 1200 31 | 32 | /** 33 | * The configuration structure for #SmolRTSP_NalTransport. 34 | */ 35 | typedef struct { 36 | /** 37 | * The maximum size of an H.264 NAL unit (including the header). 38 | */ 39 | size_t max_h264_nalu_size; 40 | 41 | /** 42 | * The maximum size of an H.265 NAL unit (including the header). 43 | */ 44 | size_t max_h265_nalu_size; 45 | } SmolRTSP_NalTransportConfig; 46 | 47 | /** 48 | * Returns the default #SmolRTSP_NalTransportConfig. 49 | * 50 | * The default values are: 51 | * 52 | * - `max_h264_nalu_size` is #SMOLRTSP_MAX_H264_NALU_SIZE. 53 | * - `max_h265_nalu_size` is #SMOLRTSP_MAX_H265_NALU_SIZE. 54 | */ 55 | SmolRTSP_NalTransportConfig 56 | SmolRTSP_NalTransportConfig_default(void) SMOLRTSP_PRIV_MUST_USE; 57 | 58 | /** 59 | * An RTP/NAL data transport. 60 | */ 61 | typedef struct SmolRTSP_NalTransport SmolRTSP_NalTransport; 62 | 63 | /** 64 | * Creates a new RTP/NAL transport with the default configuration. 65 | * 66 | * @param[in] t The underlying RTP transport. 67 | * 68 | * @pre `t != NULL` 69 | */ 70 | SmolRTSP_NalTransport * 71 | SmolRTSP_NalTransport_new(SmolRTSP_RtpTransport *t) SMOLRTSP_PRIV_MUST_USE; 72 | 73 | /** 74 | * Creates a new RTP/NAL transport with a custom configuration. 75 | * 76 | * @param[in] t The underlying RTP transport. 77 | * @param[in] config The transmission configuration structure. 78 | * 79 | * @pre `t != NULL` 80 | */ 81 | SmolRTSP_NalTransport *SmolRTSP_NalTransport_new_with_config( 82 | SmolRTSP_RtpTransport *t, 83 | SmolRTSP_NalTransportConfig config) SMOLRTSP_PRIV_MUST_USE; 84 | 85 | /** 86 | * Sends an RTP/NAL packet. 87 | * 88 | * If @p nalu is larger than the limit values from #SmolRTSP_NalTransportConfig 89 | * (configured via #SmolRTSP_NalTransport_new), 90 | * @p nalu will be 91 | * [fragmented](https://datatracker.ietf.org/doc/html/rfc6184#section-5.8). 92 | * 93 | * @param[out] self The RTP/NAL transport for sending this packet. 94 | * @param[in] ts The RTP timestamp for this packet. 95 | * @param[in] nalu The NAL unit of this RTP packet. 96 | * 97 | * @pre `self != NULL` 98 | * 99 | * @return -1 if an I/O error occurred and sets `errno` appropriately, 0 on 100 | * success. 101 | */ 102 | int SmolRTSP_NalTransport_send_packet( 103 | SmolRTSP_NalTransport *self, SmolRTSP_RtpTimestamp ts, 104 | SmolRTSP_NalUnit nalu) SMOLRTSP_PRIV_MUST_USE; 105 | 106 | /** 107 | * Implements #SmolRTSP_Droppable_IFACE for #SmolRTSP_NalTransport. 108 | * 109 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 110 | * usage. 111 | */ 112 | declImplExtern99(SmolRTSP_Droppable, SmolRTSP_NalTransport); 113 | 114 | bool SmolRTSP_NalTransport_is_full(SmolRTSP_NalTransport *self); 115 | -------------------------------------------------------------------------------- /src/types/rtp.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #define RTP_HEADER_VERSION_SHIFT 6 9 | #define RTP_HEADER_PADDING_SHIFT 5 10 | #define RTP_HEADER_EXTENSION_SHIFT 4 11 | #define RTP_HEADER_MARKER_SHIFT 7 12 | 13 | size_t SmolRTSP_RtpHeader_size(SmolRTSP_RtpHeader self) { 14 | static const size_t version_bits = 2, padding_bits = 1, extension_bits = 1, 15 | csrc_count_bits = 4, marker_bits = 1, 16 | payload_ty_bits = 7, sequence_number_bits = 16, 17 | timestamp_bits = 32, ssrc_bits = 32; 18 | 19 | static const size_t csrc_size = 4; 20 | 21 | size_t size = (version_bits + padding_bits + extension_bits + 22 | csrc_count_bits + marker_bits + payload_ty_bits + 23 | sequence_number_bits + timestamp_bits + ssrc_bits) / 24 | 8 + 25 | (self.csrc_count * csrc_size); 26 | 27 | if (self.extension) { 28 | size += sizeof(self.extension_profile) + 29 | sizeof(self.extension_payload_len) + 30 | self.extension_payload_len * sizeof(uint32_t); 31 | } 32 | 33 | return size; 34 | } 35 | 36 | uint8_t *SmolRTSP_RtpHeader_serialize( 37 | SmolRTSP_RtpHeader self, uint8_t buffer[restrict]) { 38 | assert(buffer); 39 | 40 | uint8_t *buffer_backup = buffer; 41 | 42 | /* 43 | * 0 1 2 3 44 | * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 45 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 46 | * |V=2|P|X| CC |M| PT | sequence number | 47 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 48 | * | timestamp | 49 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 50 | * | synchronization source (SSRC) identifier | 51 | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 52 | * | contributing source (CSRC) identifiers | 53 | * | .... | 54 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 55 | */ 56 | 57 | // Serialize self.version, self.padding, self.extension, and self.csrc_count 58 | // into the first byte. 59 | *buffer = (self.version << RTP_HEADER_VERSION_SHIFT) | (self.csrc_count); 60 | if (self.padding) { 61 | *buffer |= ((uint8_t)1 << RTP_HEADER_PADDING_SHIFT); 62 | } 63 | 64 | if (self.extension) { 65 | *buffer |= ((uint8_t)1 << RTP_HEADER_EXTENSION_SHIFT); 66 | } 67 | 68 | buffer++; 69 | 70 | // Serialize self.marker and self.payload_ty into the second byte. 71 | *buffer = self.payload_ty; 72 | if (self.marker) { 73 | *buffer |= ((uint8_t)1 << RTP_HEADER_MARKER_SHIFT); 74 | } 75 | 76 | buffer++; 77 | 78 | buffer = SLICE99_APPEND(buffer, self.sequence_number); 79 | buffer = SLICE99_APPEND(buffer, self.timestamp); 80 | buffer = SLICE99_APPEND(buffer, self.ssrc); 81 | 82 | for (uint8_t i = 0; i < self.csrc_count; i++) { 83 | buffer = SLICE99_APPEND(buffer, self.csrc[i]); 84 | } 85 | 86 | if (self.extension) { 87 | buffer = SLICE99_APPEND(buffer, self.extension_profile); 88 | buffer = SLICE99_APPEND(buffer, self.extension_payload_len); 89 | 90 | for (uint16_t i = 0; i < self.extension_payload_len * sizeof(uint32_t); 91 | i++) { 92 | buffer = SLICE99_APPEND(buffer, self.extension_payload[i]); 93 | } 94 | } 95 | 96 | return buffer_backup; 97 | } 98 | -------------------------------------------------------------------------------- /src/context.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | struct SmolRTSP_Context { 11 | SmolRTSP_Writer writer; 12 | uint32_t cseq; 13 | SmolRTSP_HeaderMap header_map; 14 | SmolRTSP_MessageBody body; 15 | ssize_t ret; 16 | }; 17 | 18 | SmolRTSP_Context *SmolRTSP_Context_new(SmolRTSP_Writer w, uint32_t cseq) { 19 | assert(w.self && w.vptr); 20 | 21 | SmolRTSP_Context *self = malloc(sizeof *self); 22 | assert(self); 23 | self->writer = w; 24 | self->cseq = cseq; 25 | self->header_map = SmolRTSP_HeaderMap_empty(); 26 | self->body = SmolRTSP_MessageBody_empty(); 27 | self->ret = 0; 28 | 29 | return self; 30 | } 31 | 32 | SmolRTSP_Writer SmolRTSP_Context_get_writer(const SmolRTSP_Context *ctx) { 33 | assert(ctx); 34 | return ctx->writer; 35 | } 36 | 37 | uint32_t SmolRTSP_Context_get_cseq(const SmolRTSP_Context *ctx) { 38 | assert(ctx); 39 | return ctx->cseq; 40 | } 41 | 42 | ssize_t SmolRTSP_Context_get_ret(const SmolRTSP_Context *ctx) { 43 | assert(ctx); 44 | return ctx->ret; 45 | } 46 | 47 | void smolrtsp_vheader( 48 | SmolRTSP_Context *ctx, CharSlice99 key, const char *restrict fmt, 49 | va_list list) { 50 | assert(ctx); 51 | assert(fmt); 52 | 53 | va_list list_copy; 54 | va_copy(list_copy, list); 55 | 56 | const int space_required = vsnprintf(NULL, 0, fmt, list_copy); 57 | assert(space_required > 0); 58 | char *value = malloc(space_required + 1 /* null character */); 59 | assert(value); 60 | 61 | const int bytes_written __attribute__((unused)) = 62 | vsprintf(value, fmt, list); 63 | assert(space_required == bytes_written); 64 | 65 | const SmolRTSP_Header h = {key, CharSlice99_from_str(value)}; 66 | SmolRTSP_HeaderMap_append(&ctx->header_map, h); 67 | } 68 | 69 | void smolrtsp_header( 70 | SmolRTSP_Context *ctx, CharSlice99 key, const char *restrict fmt, ...) { 71 | assert(ctx); 72 | assert(fmt); 73 | 74 | va_list ap; 75 | va_start(ap, fmt); 76 | smolrtsp_vheader(ctx, key, fmt, ap); 77 | va_end(ap); 78 | } 79 | 80 | void smolrtsp_body(SmolRTSP_Context *ctx, SmolRTSP_MessageBody body) { 81 | assert(ctx); 82 | ctx->body = body; 83 | } 84 | 85 | ssize_t smolrtsp_respond( 86 | SmolRTSP_Context *ctx, SmolRTSP_StatusCode code, const char *reason) { 87 | assert(ctx); 88 | assert(reason); 89 | 90 | const SmolRTSP_Response response = { 91 | .start_line = 92 | { 93 | .version = {.major = 1, .minor = 0}, 94 | .code = code, 95 | .reason = CharSlice99_from_str((char *)reason), 96 | }, 97 | .header_map = ctx->header_map, 98 | .body = ctx->body, 99 | .cseq = ctx->cseq, 100 | }; 101 | 102 | ctx->ret = SmolRTSP_Response_serialize(&response, ctx->writer); 103 | return ctx->ret; 104 | } 105 | 106 | ssize_t smolrtsp_respond_ok(SmolRTSP_Context *ctx) { 107 | assert(ctx); 108 | return smolrtsp_respond(ctx, SMOLRTSP_STATUS_OK, "OK"); 109 | } 110 | 111 | ssize_t smolrtsp_respond_internal_error(SmolRTSP_Context *ctx) { 112 | assert(ctx); 113 | return smolrtsp_respond( 114 | ctx, SMOLRTSP_STATUS_INTERNAL_SERVER_ERROR, "Internal error"); 115 | } 116 | 117 | void SmolRTSP_Context_drop(VSelf) { 118 | VSELF(SmolRTSP_Context); 119 | assert(self); 120 | 121 | for (size_t i = 0; i < self->header_map.len; i++) { 122 | free(self->header_map.headers[i].value.ptr); 123 | } 124 | 125 | free(self); 126 | } 127 | 128 | implExtern(SmolRTSP_Droppable, SmolRTSP_Context); 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![OpenIPC Logo](https://cdn.themactep.com/images/logo_openipc.png) 2 | 3 | # SmolRTSP 4 | [![CI](https://github.com/OpenIPC/smolrtsp/workflows/C/C++%20CI/badge.svg)](https://github.com/OpenIPC/smolrtsp/actions) 5 | [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://openipc.github.io/smolrtsp/) 6 | 7 | SmolRTSP is a simple [RTSP 1.0] server library tailored for embedded devices, such as IP cameras. It supports both TCP and UDP, allows any payload format, and provides a convenient and flexible API. 8 | 9 | [RTSP 1.0]: https://datatracker.ietf.org/doc/html/rfc2326 10 | 11 | ## Highlights 12 | 13 | - **Small.** SmolRTSP is designed for use in embedded systems (e.g., IP cameras). 14 | - **Unopinionated.** You can use SmolRTSP with bare POSIX sockets, [libevent], or any other network framework. 15 | - **Zero-copy.** SmolRTSP does not allocate or copy data while parsing. 16 | - **Battle-tested.** SmolRTSP is used by [Majestic], an IP camera streamer developed by [OpenIPC]. 17 | 18 | [libevent]: https://libevent.org/ 19 | [array slices]: https://github.com/hirrolot/slice99 20 | [Majestic]: https://openipc.github.io/wiki/en/majestic-streamer.html 21 | [OpenIPC]: https://openipc.org/ 22 | 23 | ## Features 24 | 25 | - Supported protocols: 26 | - [x] RTSP 1.0 ([RFC 2326]) 27 | - [x] RTP ([RFC 3550]) 28 | - [x] RTP over TCP (interleaved binary data) 29 | - [x] RTP over UDP 30 | - [x] SDP ([RFC 4566]) 31 | - [ ] RTCP 32 | - Supported RTP payload formats: 33 | - [x] H.264 ([RFC 6184]) 34 | - [x] H.265 ([RFC 7798]) 35 | 36 | [RFC 3550]: https://datatracker.ietf.org/doc/html/rfc3550 37 | [RFC 4566]: https://datatracker.ietf.org/doc/html/rfc4566 38 | [RFC 2326]: https://datatracker.ietf.org/doc/html/rfc2326 39 | [RFC 6184]: https://datatracker.ietf.org/doc/html/rfc6184 40 | [RFC 7798]: https://datatracker.ietf.org/doc/html/rfc7798 41 | 42 | ## Installation 43 | 44 | If you use CMake, the recommended way is [`FetchContent`]: 45 | 46 | [`FetchContent`]: https://cmake.org/cmake/help/latest/module/FetchContent.html 47 | 48 | ```cmake 49 | include(FetchContent) 50 | 51 | FetchContent_Declare( 52 | smolrtsp 53 | URL https://github.com/OpenIPC/smolrtsp/archive/refs/tags/v1.2.3.tar.gz # v1.2.3 54 | ) 55 | 56 | FetchContent_MakeAvailable(smolrtsp) 57 | 58 | target_link_libraries(MyProject smolrtsp) 59 | ``` 60 | 61 | ### Options 62 | 63 | | Option | Description | Default | 64 | |--------|-------------|---------| 65 | | `SMOLRTSP_SHARED` | Build a shared library instead of static. | `OFF` | 66 | | `SMOLRTSP_FULL_MACRO_EXPANSION` | Show full macro expansion backtraces (**DANGEROUS**: may impair diagnostics and slow down compilation). | `OFF` | 67 | 68 | ## Usage 69 | 70 | A simple example server that streams H.264 video and G.711 Mu-Law audio can be found at [`examples/server.c`](examples/server.c). 71 | 72 | ![server demo](media/example-server-demo.png) 73 | 74 | Run it as follows: 75 | 76 | ``` 77 | $ mkdir examples/build 78 | $ cd examples/build 79 | $ cmake .. && cmake --build . 80 | $ sudo ./server 81 | ``` 82 | 83 | Then open a new terminal window to start playing: 84 | 85 | ``` 86 | $ ffplay rtsp://localhost 87 | ``` 88 | 89 | ## Integration 90 | 91 | SmolRTSP is agnostic to a network backend: you can run it atop of any network/event loop framework. Currently, we support the following integrations: 92 | 93 | - [`OpenIPC/smolrtsp-libevent`](https://github.com/OpenIPC/smolrtsp-libevent) ([libevent](https://libevent.org/)) 94 | 95 | Feel free to extend this list with your own integration code. 96 | 97 | ## Release procedure 98 | 99 | 1. Update the `PROJECT_NUMBER` field in `Doxyfile`. 100 | 2. Update `CHANGELOG.md`. 101 | 3. Release the project in [GitHub Releases]. 102 | 103 | [GitHub Releases]: https://github.com/OpenIPC/smolrtsp/releases 104 | -------------------------------------------------------------------------------- /include/smolrtsp/context.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief A request context. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | /** 21 | * An opaque type used to conveniently respond to RTSP clients. 22 | */ 23 | typedef struct SmolRTSP_Context SmolRTSP_Context; 24 | 25 | /** 26 | * Creates a new SmolRTSP context. 27 | * 28 | * @param[in] w The writer to be provided with the response. 29 | * @param[in] cseq The sequence number for an RTSP request/response pair. 30 | * 31 | * @pre `w.self && w.vptr` 32 | */ 33 | SmolRTSP_Context * 34 | SmolRTSP_Context_new(SmolRTSP_Writer w, uint32_t cseq) SMOLRTSP_PRIV_MUST_USE; 35 | 36 | /** 37 | * Retrieves the writer specified in #SmolRTSP_Context_new. 38 | * 39 | * @pre `ctx != NULL` 40 | */ 41 | SmolRTSP_Writer 42 | SmolRTSP_Context_get_writer(const SmolRTSP_Context *ctx) SMOLRTSP_PRIV_MUST_USE; 43 | 44 | /** 45 | * Retrieves `cseq` specified in #SmolRTSP_Context_new. 46 | * 47 | * @pre `ctx != NULL` 48 | */ 49 | uint32_t 50 | SmolRTSP_Context_get_cseq(const SmolRTSP_Context *ctx) SMOLRTSP_PRIV_MUST_USE; 51 | 52 | /** 53 | * Retrieves the RTSP respond return value. 54 | * 55 | * If you have not responded yet, the result is 0. 56 | * 57 | * @pre `ctx != NULL` 58 | */ 59 | ssize_t 60 | SmolRTSP_Context_get_ret(const SmolRTSP_Context *ctx) SMOLRTSP_PRIV_MUST_USE; 61 | 62 | /** 63 | * Appends an RTSP header to the request context. 64 | * 65 | * @param[out] ctx The request context to modify. 66 | * @param[in] key The header key. 67 | * @param[in] fmt The `printf`-like format string (header value). 68 | * @param[in] list The variadic function arguments. 69 | * 70 | * @pre `ctx != NULL` 71 | * @pre @p ctx must contain strictly less than #SMOLRTSP_HEADER_MAP_CAPACITY 72 | * headers. 73 | * @pre `fmt != NULL` 74 | */ 75 | void smolrtsp_vheader( 76 | SmolRTSP_Context *ctx, CharSlice99 key, const char *restrict fmt, 77 | va_list list) SMOLRTSP_PRIV_GCC_ATTR(format(printf, 3, 0)); 78 | 79 | /** 80 | * The #smolrtsp_vheader twin. 81 | */ 82 | void smolrtsp_header( 83 | SmolRTSP_Context *ctx, CharSlice99 key, const char *restrict fmt, ...) 84 | SMOLRTSP_PRIV_GCC_ATTR(format(printf, 3, 4)); 85 | 86 | /** 87 | * Sets an RTSP body in the request context. 88 | * 89 | * @param[out] ctx The request context to modify. 90 | * @param[in] body The RTSP body. 91 | * 92 | * @pre `ctx != NULL` 93 | */ 94 | void smolrtsp_body(SmolRTSP_Context *ctx, SmolRTSP_MessageBody body); 95 | 96 | /** 97 | * Writes an RTSP response to the underlying writer. 98 | * 99 | * The `CSeq` and `Content-Length` headers will be written automatically as 100 | * first headers. 101 | * 102 | * @param[out] ctx The request context to write the response to. 103 | * @param[in] code The RTSP status code. 104 | * @param[in] reason The RTSP reason phrase. 105 | * 106 | * @pre `ctx != NULL` 107 | * @pre @p reason is a null-terminated string. 108 | * 109 | * @return The number of bytes written or a negative value on error. 110 | */ 111 | ssize_t smolrtsp_respond( 112 | SmolRTSP_Context *ctx, SmolRTSP_StatusCode code, const char *reason); 113 | 114 | /** 115 | * A shortcut for `smolrtsp_respond(ctx, SMOLRTSP_STATUS_OK, "OK")`. 116 | */ 117 | ssize_t smolrtsp_respond_ok(SmolRTSP_Context *ctx); 118 | 119 | /** 120 | * A shortcut for `smolrtsp_respond(ctx, SMOLRTSP_STATUS_INTERNAL_SERVER_ERROR, 121 | * "Internal error")`. 122 | */ 123 | ssize_t smolrtsp_respond_internal_error(SmolRTSP_Context *ctx); 124 | 125 | /** 126 | * Implements #SmolRTSP_Droppable_IFACE for #SmolRTSP_Context. 127 | * 128 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 129 | * usage. 130 | */ 131 | declImplExtern99(SmolRTSP_Droppable, SmolRTSP_Context); 132 | -------------------------------------------------------------------------------- /tests/context.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | TEST context_creation(void) { 8 | char buffer[32] = {0}; 9 | const SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 10 | 11 | const uint32_t cseq = 123; 12 | 13 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(w, cseq); 14 | 15 | const SmolRTSP_Writer got_writer = SmolRTSP_Context_get_writer(ctx); 16 | ASSERT_EQ(w.self, got_writer.self); 17 | ASSERT_EQ(w.vptr, got_writer.vptr); 18 | 19 | ASSERT_EQ(cseq, SmolRTSP_Context_get_cseq(ctx)); 20 | ASSERT_EQ(0, SmolRTSP_Context_get_ret(ctx)); 21 | 22 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 23 | PASS(); 24 | } 25 | 26 | TEST respond_empty(void) { 27 | char buffer[512] = {0}; 28 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 29 | const uint32_t cseq = 456; 30 | 31 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(w, cseq); 32 | 33 | const char *expected = 34 | "RTSP/1.0 404 Not found\r\n" 35 | "CSeq: 456\r\n\r\n"; 36 | 37 | ssize_t ret = smolrtsp_respond(ctx, SMOLRTSP_STATUS_NOT_FOUND, "Not found"); 38 | ASSERT_EQ((ssize_t)strlen(expected), ret); 39 | ASSERT_EQ(ret, SmolRTSP_Context_get_ret(ctx)); 40 | ASSERT_STR_EQ(expected, buffer); 41 | 42 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 43 | PASS(); 44 | } 45 | 46 | TEST respond(void) { 47 | char buffer[512] = {0}; 48 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 49 | const uint32_t cseq = 456; 50 | 51 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(w, cseq); 52 | 53 | smolrtsp_header( 54 | ctx, SMOLRTSP_HEADER_DATE, "%s, 05 Jun 1997 %d:%d:%d GMT", "Thu", 18, 55 | 57, 19); 56 | smolrtsp_header( 57 | ctx, SMOLRTSP_HEADER_CONTENT_TYPE, "application/octet-stream"); 58 | 59 | smolrtsp_body(ctx, CharSlice99_from_str("1234567890")); 60 | 61 | const char *expected = 62 | "RTSP/1.0 404 Not found\r\n" 63 | "CSeq: 456\r\n" 64 | "Content-Length: 10\r\n" 65 | "Date: Thu, 05 Jun 1997 18:57:19 GMT\r\n" 66 | "Content-Type: application/octet-stream\r\n" 67 | "\r\n1234567890"; 68 | 69 | ssize_t ret = smolrtsp_respond(ctx, SMOLRTSP_STATUS_NOT_FOUND, "Not found"); 70 | ASSERT_EQ((ssize_t)strlen(expected), ret); 71 | ASSERT_EQ(ret, SmolRTSP_Context_get_ret(ctx)); 72 | ASSERT_STR_EQ(expected, buffer); 73 | 74 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 75 | PASS(); 76 | } 77 | 78 | TEST respond_ok(void) { 79 | char buffer[512] = {0}; 80 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 81 | const uint32_t cseq = 456; 82 | 83 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(w, cseq); 84 | 85 | const char *expected = 86 | "RTSP/1.0 200 OK\r\n" 87 | "CSeq: 456\r\n\r\n"; 88 | 89 | ssize_t ret = smolrtsp_respond_ok(ctx); 90 | ASSERT_EQ((ssize_t)strlen(expected), ret); 91 | ASSERT_EQ(ret, SmolRTSP_Context_get_ret(ctx)); 92 | ASSERT_STR_EQ(expected, buffer); 93 | 94 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 95 | PASS(); 96 | } 97 | 98 | TEST respond_internal_error(void) { 99 | char buffer[512] = {0}; 100 | SmolRTSP_Writer w = smolrtsp_string_writer(buffer); 101 | const uint32_t cseq = 456; 102 | 103 | SmolRTSP_Context *ctx = SmolRTSP_Context_new(w, cseq); 104 | 105 | const char *expected = 106 | "RTSP/1.0 500 Internal error\r\n" 107 | "CSeq: 456\r\n\r\n"; 108 | 109 | ssize_t ret = smolrtsp_respond_internal_error(ctx); 110 | ASSERT_EQ((ssize_t)strlen(expected), ret); 111 | ASSERT_EQ(ret, SmolRTSP_Context_get_ret(ctx)); 112 | ASSERT_STR_EQ(expected, buffer); 113 | 114 | VTABLE(SmolRTSP_Context, SmolRTSP_Droppable).drop(ctx); 115 | PASS(); 116 | } 117 | 118 | SUITE(context) { 119 | RUN_TEST(context_creation); 120 | RUN_TEST(respond_empty); 121 | RUN_TEST(respond); 122 | RUN_TEST(respond_ok); 123 | RUN_TEST(respond_internal_error); 124 | } 125 | -------------------------------------------------------------------------------- /src/types/header_map.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../macros.h" 4 | #include "parsing.h" 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | SmolRTSP_HeaderMap SmolRTSP_HeaderMap_empty(void) { 13 | SmolRTSP_HeaderMap self; 14 | memset(self.headers, '\0', sizeof self.headers); 15 | self.len = 0; 16 | return self; 17 | } 18 | 19 | bool SmolRTSP_HeaderMap_find( 20 | const SmolRTSP_HeaderMap *restrict self, CharSlice99 key, 21 | CharSlice99 *restrict value) { 22 | assert(self); 23 | 24 | for (size_t i = 0; i < self->len; i++) { 25 | if (CharSlice99_primitive_eq(self->headers[i].key, key)) { 26 | if (value != NULL) { 27 | *value = self->headers[i].value; 28 | } 29 | return true; 30 | } 31 | } 32 | 33 | return false; 34 | } 35 | 36 | bool SmolRTSP_HeaderMap_contains_key( 37 | const SmolRTSP_HeaderMap *restrict self, CharSlice99 key) { 38 | assert(self); 39 | return SmolRTSP_HeaderMap_find(self, key, NULL); 40 | } 41 | 42 | void SmolRTSP_HeaderMap_append( 43 | SmolRTSP_HeaderMap *restrict self, SmolRTSP_Header h) { 44 | assert(self); 45 | assert(!SmolRTSP_HeaderMap_is_full(self)); 46 | 47 | self->headers[self->len] = h; 48 | self->len++; 49 | } 50 | 51 | ssize_t SmolRTSP_HeaderMap_serialize( 52 | const SmolRTSP_HeaderMap *restrict self, SmolRTSP_Writer w) { 53 | assert(self); 54 | assert(w.self && w.vptr); 55 | 56 | ssize_t result = 0; 57 | 58 | for (size_t i = 0; i < self->len; i++) { 59 | CHK_WRITE_ERR(result, SmolRTSP_Header_serialize(&self->headers[i], w)); 60 | } 61 | 62 | CHK_WRITE_ERR(result, VCALL(w, write, SMOLRTSP_CRLF)); 63 | 64 | return result; 65 | } 66 | 67 | SmolRTSP_ParseResult 68 | SmolRTSP_HeaderMap_parse(SmolRTSP_HeaderMap *restrict self, CharSlice99 input) { 69 | assert(self); 70 | 71 | const CharSlice99 backup = input; 72 | 73 | self->len = 0; 74 | 75 | while (true) { 76 | if (CharSlice99_primitive_ends_with( 77 | input, CharSlice99_from_str("\r"))) { 78 | return SmolRTSP_ParseResult_partial(); 79 | } 80 | if (CharSlice99_primitive_starts_with(input, SMOLRTSP_CRLF)) { 81 | return SmolRTSP_ParseResult_complete( 82 | (input.ptr - backup.ptr) + SMOLRTSP_CRLF.len); 83 | } 84 | if (SmolRTSP_HeaderMap_is_full(self)) { 85 | return SmolRTSP_ParseResult_Failure( 86 | SmolRTSP_ParseError_HeaderMapOverflow()); 87 | } 88 | 89 | SmolRTSP_Header header = {0}; 90 | MATCH(SmolRTSP_Header_parse(&header, input)); 91 | SmolRTSP_HeaderMap_append(self, header); 92 | } 93 | } 94 | 95 | bool SmolRTSP_HeaderMap_eq( 96 | const SmolRTSP_HeaderMap *restrict lhs, 97 | const SmolRTSP_HeaderMap *restrict rhs) { 98 | assert(lhs); 99 | assert(rhs); 100 | 101 | if (lhs->len != rhs->len) { 102 | return false; 103 | } 104 | 105 | const size_t len = lhs->len; 106 | 107 | for (size_t i = 0; i < len; i++) { 108 | if (!SmolRTSP_Header_eq(&lhs->headers[i], &rhs->headers[i])) { 109 | return false; 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | 116 | bool SmolRTSP_HeaderMap_is_full(const SmolRTSP_HeaderMap *restrict self) { 117 | assert(self); 118 | return SMOLRTSP_HEADER_MAP_CAPACITY == self->len; 119 | } 120 | 121 | int smolrtsp_scanf_header( 122 | const SmolRTSP_HeaderMap *restrict headers, CharSlice99 key, 123 | const char *restrict fmt, ...) { 124 | assert(headers); 125 | assert(fmt); 126 | 127 | CharSlice99 val; 128 | const bool val_found = SmolRTSP_HeaderMap_find(headers, key, &val); 129 | if (!val_found) { 130 | return -1; 131 | } 132 | 133 | va_list ap; 134 | va_start(ap, fmt); 135 | const int ret = vsscanf(CharSlice99_alloca_c_str(val), fmt, ap); 136 | va_end(ap); 137 | 138 | return ret; 139 | } 140 | -------------------------------------------------------------------------------- /tests/types/header_map.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #define TEST_PARSE_INIT_TYPE(result) result = SmolRTSP_HeaderMap_empty() 6 | 7 | #include "test_util.h" 8 | #include 9 | 10 | #include 11 | 12 | DEF_TEST_PARSE(SmolRTSP_HeaderMap) 13 | 14 | #define HEADER_MAP \ 15 | SmolRTSP_HeaderMap_from_array({ \ 16 | {SMOLRTSP_HEADER_CONTENT_LENGTH, CharSlice99_from_str("10")}, \ 17 | {SMOLRTSP_HEADER_ACCEPT_LANGUAGE, CharSlice99_from_str("English")}, \ 18 | {SMOLRTSP_HEADER_CONTENT_TYPE, \ 19 | CharSlice99_from_str("application/octet-stream")}, \ 20 | }) 21 | 22 | #define HEADER_MAP_STR \ 23 | "Content-Length: 10\r\nAccept-Language: English\r\nContent-Type: " \ 24 | "application/octet-stream\r\n\r\n" 25 | 26 | TEST parse_header_map(void) { 27 | TEST_PARSE(HEADER_MAP_STR, HEADER_MAP); 28 | 29 | SmolRTSP_HeaderMap result = SmolRTSP_HeaderMap_empty(); 30 | 31 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_HeaderMap_parse( 32 | &result, CharSlice99_from_str("~29838\r\n\r\n")))); 33 | ASSERT(SmolRTSP_ParseResult_is_failure(SmolRTSP_HeaderMap_parse( 34 | &result, 35 | CharSlice99_from_str("Content-Length: 10\r\n38@@2: 10\r\n\r\n")))); 36 | 37 | PASS(); 38 | } 39 | 40 | TEST serialize_header_map(void) { 41 | char buffer[500] = {0}; 42 | const SmolRTSP_HeaderMap map = HEADER_MAP; 43 | 44 | const ssize_t ret = 45 | SmolRTSP_HeaderMap_serialize(&map, smolrtsp_string_writer(buffer)); 46 | ASSERT_EQ((ssize_t)strlen(HEADER_MAP_STR), ret); 47 | ASSERT_STR_EQ(HEADER_MAP_STR, buffer); 48 | 49 | PASS(); 50 | } 51 | 52 | TEST find(void) { 53 | const SmolRTSP_HeaderMap map = HEADER_MAP; 54 | 55 | CharSlice99 content_length; 56 | const bool content_length_is_found = SmolRTSP_HeaderMap_find( 57 | &map, SMOLRTSP_HEADER_CONTENT_LENGTH, &content_length); 58 | 59 | ASSERT(content_length_is_found); 60 | ASSERT( 61 | CharSlice99_primitive_eq(content_length, CharSlice99_from_str("10"))); 62 | 63 | PASS(); 64 | } 65 | 66 | TEST contains_key(void) { 67 | const SmolRTSP_HeaderMap map = HEADER_MAP; 68 | 69 | ASSERT( 70 | SmolRTSP_HeaderMap_contains_key(&map, SMOLRTSP_HEADER_CONTENT_LENGTH)); 71 | ASSERT(!SmolRTSP_HeaderMap_contains_key(&map, SMOLRTSP_HEADER_ALLOW)); 72 | 73 | PASS(); 74 | } 75 | 76 | TEST append(void) { 77 | SmolRTSP_HeaderMap map = SmolRTSP_HeaderMap_empty(), expected = HEADER_MAP; 78 | 79 | for (size_t i = 0; i < HEADER_MAP.len; i++) { 80 | SmolRTSP_HeaderMap_append(&map, HEADER_MAP.headers[i]); 81 | } 82 | 83 | ASSERT(SmolRTSP_HeaderMap_eq(&map, &expected)); 84 | PASS(); 85 | } 86 | 87 | TEST is_full(void) { 88 | SmolRTSP_HeaderMap map = SmolRTSP_HeaderMap_empty(); 89 | 90 | ASSERT(!SmolRTSP_HeaderMap_is_full(&map)); 91 | 92 | map.len = SMOLRTSP_HEADER_MAP_CAPACITY; 93 | ASSERT(SmolRTSP_HeaderMap_is_full(&map)); 94 | 95 | PASS(); 96 | } 97 | 98 | TEST scanf_header(void) { 99 | const SmolRTSP_HeaderMap map = HEADER_MAP; 100 | 101 | size_t content_length = 0; 102 | int ret = smolrtsp_scanf_header( 103 | &map, SMOLRTSP_HEADER_CONTENT_LENGTH, "%zd", &content_length); 104 | ASSERT_EQ(1, ret); 105 | ASSERT_EQ(10, content_length); 106 | 107 | ret = smolrtsp_scanf_header(&map, SMOLRTSP_HEADER_ACCEPT_LANGUAGE, "%*d"); 108 | ASSERT_EQ(0, ret); 109 | 110 | char auth[32] = {0}; 111 | ret = smolrtsp_scanf_header( 112 | &map, SMOLRTSP_HEADER_WWW_AUTHENTICATE, "%s", auth); 113 | ASSERT_EQ(-1, ret); 114 | 115 | PASS(); 116 | } 117 | 118 | SUITE(types_header_map) { 119 | RUN_TEST(parse_header_map); 120 | RUN_TEST(serialize_header_map); 121 | RUN_TEST(find); 122 | RUN_TEST(contains_key); 123 | RUN_TEST(append); 124 | RUN_TEST(is_full); 125 | RUN_TEST(scanf_header); 126 | } 127 | -------------------------------------------------------------------------------- /include/smolrtsp/transport.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief RTSP data transport (level 4) implementations. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | 16 | /** 17 | * A transport-level RTSP data transmitter. 18 | * 19 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 20 | * usage. 21 | */ 22 | #define SmolRTSP_Transport_IFACE \ 23 | \ 24 | /* \ 25 | * Transmits a slice of I/O vectors @p bufs. \ 26 | * \ 27 | * @return -1 if an I/O error occurred and sets `errno` appropriately, 0 \ 28 | * on success. \ 29 | */ \ 30 | vfunc99(int, transmit, VSelf99, SmolRTSP_IoVecSlice bufs) \ 31 | vfunc99(bool, is_full, VSelf99) 32 | 33 | /** 34 | * The superinterfaces of #SmolRTSP_Transport_IFACE. 35 | */ 36 | #define SmolRTSP_Transport_EXTENDS (SmolRTSP_Droppable) 37 | 38 | /** 39 | * Defines the `SmolRTSP_Transport` interface. 40 | * 41 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 42 | * usage. 43 | */ 44 | interface99(SmolRTSP_Transport); 45 | 46 | /** 47 | * Creates a new TCP transport. 48 | * 49 | * @param[in] w The writer to be provided with data. 50 | * @param[in] channel_id A one-byte channel identifier, as defined in 51 | * . 52 | * 53 | * @pre `w.self && w.vptr` 54 | */ 55 | SmolRTSP_Transport smolrtsp_transport_tcp( 56 | SmolRTSP_Writer w, uint8_t channel_id, 57 | size_t max_buffer) SMOLRTSP_PRIV_MUST_USE; 58 | 59 | /** 60 | * Creates a new UDP transport. 61 | * 62 | * Strictly speaking, it can handle any datagram-oriented protocol, not 63 | * necessarily UDP. E.g., you may use a `SOCK_SEQPACKET` socket for local 64 | * communication. 65 | * 66 | * @param[in] fd The socket file descriptor to be provided with data. 67 | * 68 | * @pre `fd >= 0` 69 | */ 70 | SmolRTSP_Transport smolrtsp_transport_udp(int fd) SMOLRTSP_PRIV_MUST_USE; 71 | 72 | /** 73 | * Creates a new UDP transport with address. 74 | * 75 | * @param[in] fd The socket file descriptor to be provided with data. 76 | * @param[in] addr Pointer to address data (e.g., sockaddr_un or sockaddr_in). 77 | * @param[in] len Length of the address in bytes. 78 | * 79 | * @pre `fd >= 0` 80 | * @pre `addr && len > 0` 81 | */ 82 | SmolRTSP_Transport smolrtsp_transport_udp_address( 83 | int fd, void *addr, size_t len) SMOLRTSP_PRIV_MUST_USE; 84 | 85 | /** 86 | * Creates a new datagram socket suitable for #smolrtsp_transport_udp. 87 | * 88 | * The algorithm is: 89 | * 1. Create a socket using `socket(af, SOCK_DGRAM, 0)`. 90 | * 2. Connect this socket to @p addr with @p port. 91 | * 3. Set the `IP_PMTUDISC_WANT` option to allow IP fragmentation. 92 | * 93 | * @param[in] af The socket namespace. Can be `AF_INET` or `AF_INET6`; if none 94 | * of them, returns -1 and sets `errno` to `EAFNOSUPPORT`. 95 | * @param[in] addr The destination IP address: `struct in_addr` for `AF_INET` 96 | * and `struct in6_addr` for `AF_INET6`. 97 | * @param[in] port The destination IP port in the host byte order. 98 | * 99 | * @return A valid file descriptor or -1 on error (and sets `errno` 100 | * appropriately). 101 | */ 102 | int smolrtsp_dgram_socket(int af, const void *restrict addr, uint16_t port) 103 | SMOLRTSP_PRIV_MUST_USE; 104 | 105 | /** 106 | * Returns a pointer to the IP address of @p addr. 107 | * 108 | * Currently, only `AF_INET` and `AF_INET6` are supported. Otherwise, `NULL` is 109 | * returned. 110 | * 111 | * @pre `addr != NULL` 112 | */ 113 | void *smolrtsp_sockaddr_ip(const struct sockaddr *restrict addr) 114 | SMOLRTSP_PRIV_MUST_USE; 115 | -------------------------------------------------------------------------------- /tests/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | TEST parse_transport_config(void) { 9 | SmolRTSP_TransportConfig config; 10 | memset(&config, '\0', sizeof config); 11 | 12 | int ret = smolrtsp_parse_transport( 13 | &config, 14 | CharSlice99_from_str( 15 | "RTP/AVP/UDP;unicast;client_port=3056-3057;interleaved=4-5")); 16 | ASSERT_EQ(0, ret); 17 | ASSERT_EQ(SmolRTSP_LowerTransport_UDP, config.lower); 18 | ASSERT(config.unicast); 19 | ASSERT(!config.multicast); 20 | 21 | match(config.interleaved) { 22 | of(SmolRTSP_ChannelPair_Some, val) { 23 | ASSERT_EQ(4, val->rtp_channel); 24 | ASSERT_EQ(5, val->rtcp_channel); 25 | } 26 | otherwise FAIL(); 27 | } 28 | 29 | match(config.client_port) { 30 | of(SmolRTSP_PortPair_Some, val) { 31 | ASSERT_EQ(3056, val->rtp_port); 32 | ASSERT_EQ(3057, val->rtcp_port); 33 | } 34 | otherwise FAIL(); 35 | } 36 | 37 | PASS(); 38 | } 39 | 40 | TEST parse_transport_minimal(void) { 41 | SmolRTSP_TransportConfig config; 42 | memset(&config, '\0', sizeof config); 43 | 44 | #define CHECK_REST \ 45 | do { \ 46 | ASSERT(!config.unicast); \ 47 | ASSERT(!config.multicast); \ 48 | ASSERT(MATCHES(config.interleaved, SmolRTSP_ChannelPair_None)); \ 49 | ASSERT(MATCHES(config.client_port, SmolRTSP_PortPair_None)); \ 50 | } while (0) 51 | 52 | int ret = 53 | smolrtsp_parse_transport(&config, CharSlice99_from_str("RTP/AVP")); 54 | ASSERT_EQ(0, ret); 55 | ASSERT_EQ(SmolRTSP_LowerTransport_UDP, config.lower); 56 | CHECK_REST; 57 | 58 | ret = 59 | smolrtsp_parse_transport(&config, CharSlice99_from_str("RTP/AVP/TCP")); 60 | ASSERT_EQ(0, ret); 61 | ASSERT_EQ(SmolRTSP_LowerTransport_TCP, config.lower); 62 | CHECK_REST; 63 | 64 | ret = 65 | smolrtsp_parse_transport(&config, CharSlice99_from_str("RTP/AVP/UDP")); 66 | ASSERT_EQ(0, ret); 67 | ASSERT_EQ(SmolRTSP_LowerTransport_UDP, config.lower); 68 | CHECK_REST; 69 | 70 | #undef CHECK_REST 71 | 72 | PASS(); 73 | } 74 | 75 | TEST parse_transport_trailing_semicolon(void) { 76 | SmolRTSP_TransportConfig config; 77 | memset(&config, '\0', sizeof config); 78 | 79 | int ret = smolrtsp_parse_transport( 80 | &config, 81 | CharSlice99_from_str( 82 | "RTP/AVP/UDP;unicast;client_port=3056-3057;interleaved=4-5;")); 83 | ASSERT_EQ(0, ret); 84 | ASSERT_EQ(SmolRTSP_LowerTransport_UDP, config.lower); 85 | ASSERT(config.unicast); 86 | ASSERT(!config.multicast); 87 | 88 | match(config.interleaved) { 89 | of(SmolRTSP_ChannelPair_Some, val) { 90 | ASSERT_EQ(4, val->rtp_channel); 91 | ASSERT_EQ(5, val->rtcp_channel); 92 | } 93 | otherwise FAIL(); 94 | } 95 | 96 | match(config.client_port) { 97 | of(SmolRTSP_PortPair_Some, val) { 98 | ASSERT_EQ(3056, val->rtp_port); 99 | ASSERT_EQ(3057, val->rtcp_port); 100 | } 101 | otherwise FAIL(); 102 | } 103 | 104 | PASS(); 105 | } 106 | 107 | TEST interleaved_header(void) { 108 | uint8_t channel_id = 123; 109 | uint16_t payload_len = 54321; 110 | 111 | const uint32_t binary = 112 | smolrtsp_interleaved_header(channel_id, payload_len); 113 | 114 | ASSERT_EQ(0xd4317b24, binary); 115 | 116 | PASS(); 117 | } 118 | 119 | TEST parse_interleaved_header(void) { 120 | const uint8_t data[] = {'$', 42, 0b00110000, 0b00111001}; 121 | uint8_t channel_id = 0; 122 | uint16_t payload_len = 0; 123 | 124 | smolrtsp_parse_interleaved_header(data, &channel_id, &payload_len); 125 | 126 | ASSERT_EQ_FMT(42, channel_id, "%" PRIu8); 127 | ASSERT_EQ_FMT(12345, payload_len, "%" PRIu16); 128 | 129 | PASS(); 130 | } 131 | 132 | SUITE(util) { 133 | RUN_TEST(parse_transport_config); 134 | RUN_TEST(parse_transport_minimal); 135 | RUN_TEST(parse_transport_trailing_semicolon); 136 | RUN_TEST(interleaved_header); 137 | RUN_TEST(parse_interleaved_header); 138 | } 139 | -------------------------------------------------------------------------------- /include/smolrtsp/util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Utilitary stuff. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | /** 16 | * Carriage-return + new-line represented as a data slice. 17 | */ 18 | #define SMOLRTSP_CRLF (CharSlice99_from_str("\r\n")) 19 | 20 | /** 21 | * The default RTSP port. 22 | */ 23 | #define SMOLRTSP_DEFAULT_PORT 554 24 | 25 | /** 26 | * An RTSP lower transport. 27 | */ 28 | typedef enum { 29 | /** 30 | * TCP. 31 | */ 32 | SmolRTSP_LowerTransport_TCP, 33 | 34 | /** 35 | * UDP. 36 | */ 37 | SmolRTSP_LowerTransport_UDP, 38 | } SmolRTSP_LowerTransport; 39 | 40 | /** 41 | * Converts @p self to a string representation (`"TCP"` for 42 | * #SmolRTSP_LowerTransport_TCP and `"UDP"` for #SmolRTSP_LowerTransport_UDP). 43 | */ 44 | const char *SmolRTSP_LowerTransport_str(SmolRTSP_LowerTransport self); 45 | 46 | /** 47 | * An RTP/RTCP port pair specified as a range, e.g., `client_port=3456-3457`. 48 | */ 49 | typedef struct { 50 | /** 51 | * The RTP port. 52 | */ 53 | uint16_t rtp_port; 54 | 55 | /** 56 | * The RTCP port. 57 | */ 58 | uint16_t rtcp_port; 59 | } SmolRTSP_PortPair; 60 | 61 | /** 62 | * Defines `SmolRTSP_PortPairOption`. 63 | * 64 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 65 | */ 66 | SMOLRTSP_DEF_OPTION(SmolRTSP_PortPair); 67 | 68 | /** 69 | * An RTP/RTCP channel pair specified as a range, e.g., `interleaved=4-5`. 70 | */ 71 | typedef struct { 72 | /** 73 | * The RTP channel identifier. 74 | */ 75 | uint8_t rtp_channel; 76 | 77 | /** 78 | * The RTCP channel identifier. 79 | */ 80 | uint8_t rtcp_channel; 81 | } SmolRTSP_ChannelPair; 82 | 83 | /** 84 | * Defines `SmolRTSP_ChannelPairOption`. 85 | * 86 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 87 | */ 88 | SMOLRTSP_DEF_OPTION(SmolRTSP_ChannelPair); 89 | 90 | /** 91 | * The RTSP transport configuration. 92 | * 93 | * @see 94 | */ 95 | typedef struct { 96 | /** 97 | * The lower level transport (TCP or UDP). 98 | */ 99 | SmolRTSP_LowerTransport lower; 100 | 101 | /** 102 | * True if the `unicast` parameter is present. 103 | */ 104 | bool unicast; 105 | 106 | /** 107 | * True if the `multicast` parameter is present. 108 | */ 109 | bool multicast; 110 | 111 | /** 112 | * The `interleaved` parameter, if present. 113 | */ 114 | SmolRTSP_ChannelPairOption interleaved; 115 | 116 | /** 117 | * The `client_port` parameter, if present. 118 | */ 119 | SmolRTSP_PortPairOption client_port; 120 | } SmolRTSP_TransportConfig; 121 | 122 | /** 123 | * Parses the 124 | * [`Transport`](https://datatracker.ietf.org/doc/html/rfc2326#section-12.39) 125 | * header. 126 | * 127 | * @param[out] config The result of parsing. It remains unchanged on failure. 128 | * @param[in] header_value The value of the `Transport` header. 129 | * 130 | * @return 0 on success, -1 on failure. 131 | * 132 | * @pre `config != NULL` 133 | */ 134 | int smolrtsp_parse_transport( 135 | SmolRTSP_TransportConfig *restrict config, 136 | CharSlice99 header_value) SMOLRTSP_PRIV_MUST_USE; 137 | 138 | /** 139 | * Returns a four-octet interleaved binary data header. 140 | * 141 | * @param[in] channel_id The one-byte channel identifier. 142 | * @param[in] payload_len The length of the encapsulated binary data 143 | * (network byte order). 144 | * 145 | * @see 146 | */ 147 | uint32_t smolrtsp_interleaved_header(uint8_t channel_id, uint16_t payload_len) 148 | SMOLRTSP_PRIV_MUST_USE; 149 | 150 | /** 151 | * Parses an four-octet interleaved binary data header @p data. 152 | * 153 | * @param[in] data The header to parse. 154 | * @param[out] channel_id The one-byte channel identifier. 155 | * @param[out] payload_len The length of the encapsulated binary data 156 | * (host byte order). 157 | * 158 | * @pre `channel_id != NULL` 159 | * @pre `payload_len != NULL` 160 | * 161 | * @see 162 | */ 163 | void smolrtsp_parse_interleaved_header( 164 | const uint8_t data[restrict static 4], uint8_t *restrict channel_id, 165 | uint16_t *restrict payload_len); 166 | -------------------------------------------------------------------------------- /src/types/error.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const char *SmolRTSP_ParseType_str(SmolRTSP_ParseType self) { 4 | switch (self) { 5 | case SmolRTSP_ParseType_Int: 6 | return "Integer"; 7 | case SmolRTSP_ParseType_Ident: 8 | return "Identifier"; 9 | case SmolRTSP_ParseType_HeaderName: 10 | return "Header name"; 11 | default: 12 | return "Unknown"; 13 | } 14 | } 15 | 16 | #define MAX_STR 10 17 | #define TRUNCATE_STR(str) \ 18 | ((str).len <= MAX_STR ? (str) : CharSlice99_sub((str), 0, MAX_STR)) 19 | 20 | int SmolRTSP_ParseError_print(SmolRTSP_ParseError self, SmolRTSP_Writer w) { 21 | assert(w.self && w.vptr); 22 | 23 | match(self) { 24 | of(SmolRTSP_ParseError_ContentLength, value) { 25 | return SMOLRTSP_WRITE_SLICES( 26 | w, { 27 | CharSlice99_from_str("Invalid Content-Length `"), 28 | TRUNCATE_STR(*value), 29 | CharSlice99_from_str("`"), 30 | }); 31 | } 32 | of(SmolRTSP_ParseError_StrMismatch, expected, actual) { 33 | return SMOLRTSP_WRITE_SLICES( 34 | w, { 35 | CharSlice99_from_str("String mismatch: expected `"), 36 | TRUNCATE_STR(*expected), 37 | CharSlice99_from_str("`, found `"), 38 | TRUNCATE_STR(*actual), 39 | CharSlice99_from_str("`"), 40 | }); 41 | } 42 | of(SmolRTSP_ParseError_TypeMismatch, kind, str) { 43 | return SMOLRTSP_WRITE_SLICES( 44 | w, 45 | { 46 | CharSlice99_from_str("Type mismatch: expected "), 47 | CharSlice99_from_str((char *)SmolRTSP_ParseType_str(*kind)), 48 | CharSlice99_from_str(", found `"), 49 | TRUNCATE_STR(*str), 50 | CharSlice99_from_str("`"), 51 | }); 52 | } 53 | of(SmolRTSP_ParseError_HeaderMapOverflow) { 54 | return VCALL( 55 | w, write, 56 | CharSlice99_from_str( 57 | "Not enough space left in the header map")); 58 | } 59 | of(SmolRTSP_ParseError_MissingCSeq) { 60 | return VCALL(w, write, CharSlice99_from_str("Missing CSeq")); 61 | } 62 | of(SmolRTSP_ParseError_InvalidCSeq, value) { 63 | return SMOLRTSP_WRITE_SLICES( 64 | w, { 65 | CharSlice99_from_str("Invalid CSeq `"), 66 | TRUNCATE_STR(*value), 67 | CharSlice99_from_str("`"), 68 | }); 69 | } 70 | } 71 | 72 | return -1; 73 | } 74 | 75 | #undef MAX_STR 76 | #undef TRUNCATE_STR 77 | 78 | bool SmolRTSP_ParseStatus_is_complete(SmolRTSP_ParseStatus self) { 79 | return MATCHES(self, SmolRTSP_ParseStatus_Complete); 80 | } 81 | 82 | bool SmolRTSP_ParseStatus_is_partial(SmolRTSP_ParseStatus self) { 83 | return MATCHES(self, SmolRTSP_ParseStatus_Partial); 84 | } 85 | 86 | SmolRTSP_ParseResult SmolRTSP_ParseResult_partial(void) { 87 | return SmolRTSP_ParseResult_Success(SmolRTSP_ParseStatus_Partial()); 88 | } 89 | 90 | SmolRTSP_ParseResult SmolRTSP_ParseResult_complete(size_t offset) { 91 | return SmolRTSP_ParseResult_Success(SmolRTSP_ParseStatus_Complete(offset)); 92 | } 93 | 94 | bool SmolRTSP_ParseResult_is_success(SmolRTSP_ParseResult self) { 95 | return MATCHES(self, SmolRTSP_ParseResult_Success); 96 | } 97 | 98 | bool SmolRTSP_ParseResult_is_failure(SmolRTSP_ParseResult self) { 99 | return !SmolRTSP_ParseResult_is_success(self); 100 | } 101 | 102 | bool SmolRTSP_ParseResult_is_partial(SmolRTSP_ParseResult self) { 103 | // Used to workaround `-Wreturn-type`. 104 | bool result = true; 105 | 106 | match(self) { 107 | of(SmolRTSP_ParseResult_Success, status) result = 108 | SmolRTSP_ParseStatus_is_partial(*status); 109 | otherwise result = false; 110 | } 111 | 112 | return result; 113 | } 114 | 115 | bool SmolRTSP_ParseResult_is_complete(SmolRTSP_ParseResult self) { 116 | // Used to workaround `-Wreturn-type`. 117 | bool result = true; 118 | 119 | match(self) { 120 | of(SmolRTSP_ParseResult_Success, status) result = 121 | SmolRTSP_ParseStatus_is_complete(*status); 122 | otherwise result = false; 123 | } 124 | 125 | return result; 126 | } 127 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(smolrtsp LANGUAGES C) 3 | 4 | # Fix the warnings about `DOWNLOAD_EXTRACT_TIMESTAMP` in newer CMake versions. 5 | if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") 6 | cmake_policy(SET CMP0135 NEW) 7 | endif() 8 | 9 | option(SMOLRTSP_SHARED "Build a shared library" OFF) 10 | option(SMOLRTSP_FULL_MACRO_EXPANSION "Show full macro expansion backtraces" OFF) 11 | 12 | include(FetchContent) 13 | 14 | FetchContent_Declare( 15 | slice99 16 | URL https://github.com/hirrolot/slice99/archive/refs/tags/v0.7.8.tar.gz 17 | ) 18 | 19 | FetchContent_Declare( 20 | metalang99 21 | URL https://github.com/hirrolot/metalang99/archive/refs/tags/v1.13.5.tar.gz 22 | ) 23 | 24 | FetchContent_Declare( 25 | datatype99 26 | URL https://github.com/hirrolot/datatype99/archive/refs/tags/v1.6.5.tar.gz 27 | ) 28 | 29 | FetchContent_Declare( 30 | interface99 31 | URL https://github.com/hirrolot/interface99/archive/refs/tags/v1.0.2.tar.gz 32 | ) 33 | 34 | FetchContent_MakeAvailable(slice99 datatype99 interface99) 35 | 36 | set(SMOLRTSP_SOURCES 37 | include/smolrtsp/types/error.h 38 | include/smolrtsp/types/header_map.h 39 | include/smolrtsp/types/header.h 40 | include/smolrtsp/types/message_body.h 41 | include/smolrtsp/types/method.h 42 | include/smolrtsp/types/reason_phrase.h 43 | include/smolrtsp/types/request_line.h 44 | include/smolrtsp/types/request_uri.h 45 | include/smolrtsp/types/request.h 46 | include/smolrtsp/types/response_line.h 47 | include/smolrtsp/types/response.h 48 | include/smolrtsp/types/rtsp_version.h 49 | include/smolrtsp/types/status_code.h 50 | include/smolrtsp/types/sdp.h 51 | include/smolrtsp/types/rtp.h 52 | include/smolrtsp/nal/h264.h 53 | include/smolrtsp/nal/h265.h 54 | include/smolrtsp/nal.h 55 | include/smolrtsp/writer.h 56 | include/smolrtsp/util.h 57 | include/smolrtsp/transport.h 58 | include/smolrtsp/rtp_transport.h 59 | include/smolrtsp/nal_transport.h 60 | include/smolrtsp/droppable.h 61 | include/smolrtsp/controller.h 62 | include/smolrtsp/io_vec.h 63 | include/smolrtsp/context.h 64 | include/smolrtsp/option.h 65 | src/types/error.c 66 | src/types/header_map.c 67 | src/types/header.c 68 | src/types/message_body.c 69 | src/types/method.c 70 | src/types/parsing.c 71 | src/types/parsing.h 72 | src/types/reason_phrase.c 73 | src/types/request_line.c 74 | src/types/request_uri.c 75 | src/types/request.c 76 | src/types/response_line.c 77 | src/types/response.c 78 | src/types/rtsp_version.c 79 | src/types/status_code.c 80 | src/types/sdp.c 81 | src/types/rtp.c 82 | src/nal/h264.c 83 | src/nal/h265.c 84 | src/nal.c 85 | src/writer.c 86 | src/writer/fd.c 87 | src/writer/file.c 88 | src/writer/string.c 89 | src/util.c 90 | src/transport/tcp.c 91 | src/transport/udp.c 92 | src/rtp_transport.c 93 | src/nal_transport.c 94 | src/io_vec.c 95 | src/controller.c 96 | src/context.c 97 | src/macros.h 98 | ) 99 | 100 | if(SMOLRTSP_SHARED) 101 | add_library(${PROJECT_NAME} SHARED ${SMOLRTSP_SOURCES}) 102 | else() 103 | add_library(${PROJECT_NAME} STATIC ${SMOLRTSP_SOURCES}) 104 | endif() 105 | 106 | if(CMAKE_C_COMPILER_ID STREQUAL "Clang") 107 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra) 108 | elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU") 109 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wno-misleading-indentation) 110 | endif() 111 | 112 | if(NOT SMOLRTSP_FULL_MACRO_EXPANSION) 113 | if(CMAKE_C_COMPILER_ID STREQUAL "Clang") 114 | target_compile_options(${PROJECT_NAME} PUBLIC -fmacro-backtrace-limit=1) 115 | elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU") 116 | target_compile_options(${PROJECT_NAME} PUBLIC -ftrack-macro-expansion=0) 117 | endif() 118 | endif() 119 | 120 | # Precompile headers that use Datatype99/Interface99. 121 | target_precompile_headers( 122 | ${PROJECT_NAME} PRIVATE 123 | include/smolrtsp/types/error.h 124 | include/smolrtsp/nal/h264.h 125 | include/smolrtsp/nal/h265.h 126 | include/smolrtsp/nal.h 127 | include/smolrtsp/writer.h 128 | include/smolrtsp/transport.h 129 | include/smolrtsp/droppable.h 130 | include/smolrtsp/controller.h 131 | include/smolrtsp/rtp_transport.h 132 | include/smolrtsp/util.h) 133 | 134 | target_include_directories(${PROJECT_NAME} PUBLIC include) 135 | target_link_libraries(${PROJECT_NAME} PUBLIC slice99 metalang99 datatype99 interface99) 136 | 137 | set_target_properties(${PROJECT_NAME} PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) 138 | -------------------------------------------------------------------------------- /src/types/request.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../macros.h" 4 | #include "parsing.h" 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | SmolRTSP_Request SmolRTSP_Request_uninit(void) { 15 | SmolRTSP_Request self; 16 | memset(&self, '\0', sizeof self); 17 | self.header_map = SmolRTSP_HeaderMap_empty(); 18 | return self; 19 | } 20 | 21 | ssize_t SmolRTSP_Request_serialize( 22 | const SmolRTSP_Request *restrict self, SmolRTSP_Writer w) { 23 | assert(self); 24 | assert(w.self && w.vptr); 25 | 26 | ssize_t result = 0; 27 | 28 | CHK_WRITE_ERR(result, SmolRTSP_RequestLine_serialize(&self->start_line, w)); 29 | 30 | if (!SmolRTSP_HeaderMap_contains_key( 31 | &self->header_map, SMOLRTSP_HEADER_C_SEQ)) { 32 | const SmolRTSP_Header cseq = { 33 | SMOLRTSP_HEADER_C_SEQ, 34 | CharSlice99_alloca_fmt("%" PRIu32, self->cseq), 35 | }; 36 | CHK_WRITE_ERR(result, SmolRTSP_Header_serialize(&cseq, w)); 37 | } 38 | 39 | if (!SmolRTSP_HeaderMap_contains_key( 40 | &self->header_map, SMOLRTSP_HEADER_CONTENT_LENGTH) && 41 | !CharSlice99_is_empty(self->body)) { 42 | const SmolRTSP_Header content_length = { 43 | SMOLRTSP_HEADER_CONTENT_LENGTH, 44 | CharSlice99_alloca_fmt("%zd", self->body.len), 45 | }; 46 | CHK_WRITE_ERR(result, SmolRTSP_Header_serialize(&content_length, w)); 47 | } 48 | 49 | CHK_WRITE_ERR(result, SmolRTSP_HeaderMap_serialize(&self->header_map, w)); 50 | if (!CharSlice99_is_empty(self->body)) { 51 | CHK_WRITE_ERR(result, VCALL(w, write, self->body)); 52 | } 53 | 54 | return result; 55 | } 56 | 57 | SmolRTSP_ParseResult 58 | SmolRTSP_Request_parse(SmolRTSP_Request *restrict self, CharSlice99 input) { 59 | assert(self); 60 | 61 | const CharSlice99 backup = input; 62 | 63 | // TODO: implement proper parsing of interleaved binary data. 64 | for (;;) { 65 | if (input.len < sizeof(uint32_t)) { 66 | return SmolRTSP_ParseResult_partial(); 67 | } 68 | 69 | if ('$' == input.ptr[0]) { 70 | uint8_t channel_id = 0; 71 | uint16_t payload_len = 0; 72 | smolrtsp_parse_interleaved_header( 73 | (const uint8_t *)input.ptr, &channel_id, &payload_len); 74 | 75 | input = CharSlice99_advance(input, sizeof(uint32_t)); 76 | 77 | if (input.len < (size_t)payload_len) { 78 | return SmolRTSP_ParseResult_partial(); 79 | } 80 | input = CharSlice99_advance(input, (size_t)payload_len); 81 | } else { 82 | break; 83 | } 84 | } 85 | 86 | MATCH(SmolRTSP_RequestLine_parse(&self->start_line, input)); 87 | MATCH(SmolRTSP_HeaderMap_parse(&self->header_map, input)); 88 | 89 | CharSlice99 content_length; 90 | size_t content_length_int = 0; 91 | const bool content_length_found = SmolRTSP_HeaderMap_find( 92 | &self->header_map, SMOLRTSP_HEADER_CONTENT_LENGTH, &content_length); 93 | 94 | if (content_length_found) { 95 | if (sscanf( 96 | CharSlice99_alloca_c_str(content_length), "%zd", 97 | &content_length_int) != 1) { 98 | return SmolRTSP_ParseResult_Failure( 99 | SmolRTSP_ParseError_ContentLength(content_length)); 100 | } 101 | } 102 | 103 | MATCH(SmolRTSP_MessageBody_parse(&self->body, input, content_length_int)); 104 | 105 | CharSlice99 cseq_value; 106 | const bool cseq_found = SmolRTSP_HeaderMap_find( 107 | &self->header_map, SMOLRTSP_HEADER_C_SEQ, &cseq_value); 108 | if (!cseq_found) { 109 | return SmolRTSP_ParseResult_Failure(SmolRTSP_ParseError_MissingCSeq()); 110 | } 111 | 112 | uint32_t cseq; 113 | if (sscanf(CharSlice99_alloca_c_str(cseq_value), "%" SCNu32, &cseq) != 1) { 114 | return SmolRTSP_ParseResult_Failure( 115 | SmolRTSP_ParseError_InvalidCSeq(cseq_value)); 116 | } 117 | 118 | self->cseq = cseq; 119 | 120 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 121 | } 122 | 123 | bool SmolRTSP_Request_eq( 124 | const SmolRTSP_Request *restrict lhs, 125 | const SmolRTSP_Request *restrict rhs) { 126 | assert(lhs); 127 | assert(rhs); 128 | 129 | return SmolRTSP_RequestLine_eq(&lhs->start_line, &rhs->start_line) && 130 | SmolRTSP_HeaderMap_eq(&lhs->header_map, &rhs->header_map) && 131 | SmolRTSP_MessageBody_eq(&lhs->body, &rhs->body) && 132 | lhs->cseq == rhs->cseq; 133 | } 134 | -------------------------------------------------------------------------------- /src/types/response.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../macros.h" 5 | #include "parsing.h" 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | SmolRTSP_Response SmolRTSP_Response_uninit(void) { 15 | SmolRTSP_Response self; 16 | memset(&self, '\0', sizeof self); 17 | self.header_map = SmolRTSP_HeaderMap_empty(); 18 | return self; 19 | } 20 | 21 | ssize_t SmolRTSP_Response_serialize( 22 | const SmolRTSP_Response *restrict self, SmolRTSP_Writer w) { 23 | assert(self); 24 | assert(w.self && w.vptr); 25 | 26 | ssize_t result = 0; 27 | 28 | CHK_WRITE_ERR( 29 | result, SmolRTSP_ResponseLine_serialize(&self->start_line, w)); 30 | 31 | if (!SmolRTSP_HeaderMap_contains_key( 32 | &self->header_map, SMOLRTSP_HEADER_C_SEQ)) { 33 | const SmolRTSP_Header cseq = { 34 | SMOLRTSP_HEADER_C_SEQ, 35 | CharSlice99_alloca_fmt("%" PRIu32, self->cseq), 36 | }; 37 | CHK_WRITE_ERR(result, SmolRTSP_Header_serialize(&cseq, w)); 38 | } 39 | 40 | if (!SmolRTSP_HeaderMap_contains_key( 41 | &self->header_map, SMOLRTSP_HEADER_CONTENT_LENGTH) && 42 | !CharSlice99_is_empty(self->body)) { 43 | const SmolRTSP_Header content_length = { 44 | SMOLRTSP_HEADER_CONTENT_LENGTH, 45 | CharSlice99_alloca_fmt("%zd", self->body.len), 46 | }; 47 | CHK_WRITE_ERR(result, SmolRTSP_Header_serialize(&content_length, w)); 48 | } 49 | 50 | CHK_WRITE_ERR(result, SmolRTSP_HeaderMap_serialize(&self->header_map, w)); 51 | if (!CharSlice99_is_empty(self->body)) { 52 | CHK_WRITE_ERR(result, VCALL(w, write, self->body)); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | SmolRTSP_ParseResult 59 | SmolRTSP_Response_parse(SmolRTSP_Response *restrict self, CharSlice99 input) { 60 | assert(self); 61 | 62 | const CharSlice99 backup = input; 63 | 64 | // TODO: implement proper parsing of interleaved binary data. 65 | for (;;) { 66 | if (input.len < sizeof(uint32_t)) { 67 | return SmolRTSP_ParseResult_partial(); 68 | } 69 | 70 | if ('$' == input.ptr[0]) { 71 | uint8_t channel_id = 0; 72 | uint16_t payload_len = 0; 73 | smolrtsp_parse_interleaved_header( 74 | (const uint8_t *)input.ptr, &channel_id, &payload_len); 75 | 76 | input = CharSlice99_advance(input, sizeof(uint32_t)); 77 | 78 | if (input.len < (size_t)payload_len) { 79 | return SmolRTSP_ParseResult_partial(); 80 | } 81 | input = CharSlice99_advance(input, (size_t)payload_len); 82 | } else { 83 | break; 84 | } 85 | } 86 | 87 | MATCH(SmolRTSP_ResponseLine_parse(&self->start_line, input)); 88 | MATCH(SmolRTSP_HeaderMap_parse(&self->header_map, input)); 89 | 90 | CharSlice99 content_length; 91 | size_t content_length_int = 0; 92 | const bool content_length_is_found = SmolRTSP_HeaderMap_find( 93 | &self->header_map, SMOLRTSP_HEADER_CONTENT_LENGTH, &content_length); 94 | 95 | if (content_length_is_found) { 96 | if (sscanf( 97 | CharSlice99_alloca_c_str(content_length), "%zd", 98 | &content_length_int) != 1) { 99 | return SmolRTSP_ParseResult_Failure( 100 | SmolRTSP_ParseError_ContentLength(content_length)); 101 | } 102 | } 103 | 104 | MATCH(SmolRTSP_MessageBody_parse(&self->body, input, content_length_int)); 105 | 106 | CharSlice99 cseq_value; 107 | const bool cseq_found = SmolRTSP_HeaderMap_find( 108 | &self->header_map, SMOLRTSP_HEADER_C_SEQ, &cseq_value); 109 | if (!cseq_found) { 110 | return SmolRTSP_ParseResult_Failure(SmolRTSP_ParseError_MissingCSeq()); 111 | } 112 | 113 | uint32_t cseq; 114 | if (sscanf(CharSlice99_alloca_c_str(cseq_value), "%" SCNu32, &cseq) != 1) { 115 | return SmolRTSP_ParseResult_Failure( 116 | SmolRTSP_ParseError_InvalidCSeq(cseq_value)); 117 | } 118 | 119 | self->cseq = cseq; 120 | 121 | return SmolRTSP_ParseResult_complete(input.ptr - backup.ptr); 122 | } 123 | 124 | bool SmolRTSP_Response_eq( 125 | const SmolRTSP_Response *restrict lhs, 126 | const SmolRTSP_Response *restrict rhs) { 127 | assert(lhs); 128 | assert(rhs); 129 | 130 | return SmolRTSP_ResponseLine_eq(&lhs->start_line, &rhs->start_line) && 131 | SmolRTSP_HeaderMap_eq(&lhs->header_map, &rhs->header_map) && 132 | SmolRTSP_MessageBody_eq(&lhs->body, &rhs->body) && 133 | lhs->cseq == rhs->cseq; 134 | } 135 | -------------------------------------------------------------------------------- /include/smolrtsp/types/header_map.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief An RTSP header map. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include 19 | 20 | /** 21 | * Creates new #SmolRTSP_HeaderMap from an array expression of #SmolRTSP_Header. 22 | */ 23 | #define SmolRTSP_HeaderMap_from_array(...) \ 24 | ((SmolRTSP_HeaderMap){ \ 25 | .headers = __VA_ARGS__, \ 26 | .len = SLICE99_ARRAY_LEN((SmolRTSP_Header[])__VA_ARGS__), \ 27 | }) 28 | 29 | /** 30 | * The maximum number of headers in #SmolRTSP_HeaderMap.headers. 31 | */ 32 | #define SMOLRTSP_HEADER_MAP_CAPACITY 32 33 | 34 | /** 35 | * An RTSP header map. 36 | */ 37 | typedef struct { 38 | /** 39 | * The pointer to an array of headers; 40 | */ 41 | SmolRTSP_Header headers[SMOLRTSP_HEADER_MAP_CAPACITY]; 42 | 43 | /** 44 | * The count of elements currently in #headers. 45 | */ 46 | size_t len; 47 | } SmolRTSP_HeaderMap; 48 | 49 | /** 50 | * Returns an empty header map suitable for further parsing. 51 | */ 52 | SmolRTSP_HeaderMap SmolRTSP_HeaderMap_empty(void) SMOLRTSP_PRIV_MUST_USE; 53 | 54 | /** 55 | * Finds a value associated with @p key within @p self. 56 | * 57 | * If @p key has been found within @p self, this function assigns @p value to 58 | * this key (no copying occurs) and returns `true`. Otherwise, returns `false` 59 | * and @p value remains unchanged. 60 | * 61 | * @param[in] self The header map to be searched for @p key. 62 | * @param[in] key The key to be searched in @p self. 63 | * @param[out] value The header value to be assigned, if found. If `NULL`, no 64 | * assignment is performed. 65 | * 66 | * @pre `self != NULL` 67 | */ 68 | bool SmolRTSP_HeaderMap_find( 69 | const SmolRTSP_HeaderMap *restrict self, CharSlice99 key, 70 | CharSlice99 *restrict value) SMOLRTSP_PRIV_MUST_USE; 71 | 72 | /** 73 | * Returns whether @p key is present in @p self. 74 | * 75 | * @pre `self != NULL` 76 | */ 77 | bool SmolRTSP_HeaderMap_contains_key( 78 | const SmolRTSP_HeaderMap *restrict self, 79 | CharSlice99 key) SMOLRTSP_PRIV_MUST_USE; 80 | 81 | /** 82 | * Appends a new header to a header map. 83 | * 84 | * @param[out] self The header map to modify. 85 | * @param[in] h The new header to be appended. 86 | * 87 | * @pre `self != NULL` 88 | * @pre `!SmolRTSP_HeaderMap_is_full(self)` 89 | */ 90 | void SmolRTSP_HeaderMap_append( 91 | SmolRTSP_HeaderMap *restrict self, SmolRTSP_Header h); 92 | 93 | /** 94 | * Serialises @p self into @p w. 95 | * 96 | * @param[in] self The instance to be serialised. 97 | * @param[in] w The writer to be provided with serialised data. 98 | * 99 | * @return The number of bytes written or a negative value on error. 100 | * 101 | * @pre `self != NULL` 102 | * @pre `w.self && w.vptr` 103 | */ 104 | ssize_t SmolRTSP_HeaderMap_serialize( 105 | const SmolRTSP_HeaderMap *restrict self, 106 | SmolRTSP_Writer w) SMOLRTSP_PRIV_MUST_USE; 107 | 108 | /** 109 | * Parses @p data to @p self. 110 | * 111 | * @pre `self != NULL` 112 | */ 113 | SmolRTSP_ParseResult SmolRTSP_HeaderMap_parse( 114 | SmolRTSP_HeaderMap *restrict self, 115 | CharSlice99 input) SMOLRTSP_PRIV_MUST_USE; 116 | 117 | /** 118 | * Tests @p lhs and @p rhs for equality. 119 | * 120 | * @pre `lhs != NULL` 121 | * @pre `rhs != NULL` 122 | */ 123 | bool SmolRTSP_HeaderMap_eq( 124 | const SmolRTSP_HeaderMap *restrict lhs, 125 | const SmolRTSP_HeaderMap *restrict rhs) SMOLRTSP_PRIV_MUST_USE; 126 | 127 | /** 128 | * Tests whether @p self is full (no more space left for an additional header) 129 | * or not. 130 | * 131 | * @return `true` if @p self is full, `false` otherwise. 132 | * 133 | * @pre `self != NULL` 134 | */ 135 | bool SmolRTSP_HeaderMap_is_full(const SmolRTSP_HeaderMap *restrict self) 136 | SMOLRTSP_PRIV_MUST_USE; 137 | 138 | /** 139 | * Attempts to parse a header. 140 | * 141 | * @param[in] headers The header map to search @p key in. 142 | * @param[in] key The header key to search for. 143 | * @param[in] fmt The `scanf-`like format string. 144 | * 145 | * @return The number of scanned parameters or -1 if @p key is not found. 146 | * 147 | * @pre `headers != NULL` 148 | * @pre `fmt != NULL` 149 | */ 150 | int smolrtsp_scanf_header( 151 | const SmolRTSP_HeaderMap *restrict headers, CharSlice99 key, 152 | const char *restrict fmt, ...) SMOLRTSP_PRIV_MUST_USE 153 | SMOLRTSP_PRIV_GCC_ATTR(format(scanf, 3, 4)); 154 | -------------------------------------------------------------------------------- /include/smolrtsp/types/rtp.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief RFC 4 | * 3550-compliant RTP implementation. 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | /** 16 | * An RTP header. 17 | * 18 | * All numerical fields must be in network byte order. 19 | */ 20 | typedef struct { 21 | /** 22 | * (2 bits) Indicates the version of the protocol. Current version is 2. 23 | */ 24 | uint8_t version; 25 | 26 | /** 27 | * (1 bit) Used to indicate if there are extra padding bytes at the end of 28 | * the RTP packet. Padding may be used to fill up a block of certain size, 29 | * for example as required by an encryption algorithm. The last byte of the 30 | * padding contains the number of padding bytes that were added (including 31 | * itself). 32 | */ 33 | bool padding; 34 | 35 | /** 36 | * (1 bit) Indicates presence of an extension header between the header and 37 | * payload data. The extension header is application or profile specific. 38 | */ 39 | bool extension; 40 | 41 | /** 42 | * (4 bits) Contains the number of CSRC identifiers (defined below) that 43 | * follow the SSRC (also defined below). 44 | */ 45 | uint8_t csrc_count; 46 | 47 | /** 48 | * (1 bit) Signaling used at the application level in a profile-specific 49 | * manner. If it is set, it means that the current data has some special 50 | * relevance for the application. 51 | */ 52 | bool marker; 53 | 54 | /** 55 | * (7 bits) Indicates the format of the payload and thus determines its 56 | * interpretation by the application. Values are profile specific and may be 57 | * dynamically assigned. 58 | */ 59 | uint8_t payload_ty; 60 | 61 | /** 62 | * (16 bits) The sequence number is incremented for each RTP data packet 63 | * sent and is to be used by the receiver to detect packet loss[1] and to 64 | * accommodate out-of-order delivery. The initial value of the sequence 65 | * number should be randomized to make known-plaintext attacks on Secure 66 | * Real-time Transport Protocol more difficult. 67 | */ 68 | uint16_t sequence_number; 69 | 70 | /** 71 | * Used by the receiver to play back the received samples at appropriate 72 | * time and interval. When several media streams are present, the timestamps 73 | * may be independent in each stream.[b] The granularity of the timing is 74 | * application specific. For example, an audio application that samples data 75 | * once every 125 μs (8 kHz, a common sample rate in digital telephony) 76 | * would use that value as its clock resolution. Video streams typically use 77 | * a 90 kHz clock. The clock granularity is one of the details that is 78 | * specified in the RTP profile for an application. 79 | */ 80 | uint32_t timestamp; 81 | 82 | /** 83 | * (32 bits) Synchronization source identifier uniquely identifies the 84 | * source of a stream. The synchronization sources within the same RTP 85 | * session will be unique. 86 | */ 87 | uint32_t ssrc; 88 | 89 | /** 90 | * (32 bits each, the number of entries is indicated by the CSRC count 91 | * field) Contributing source IDs enumerate contributing sources to a stream 92 | * which has been generated from multiple sources. 93 | */ 94 | uint32_t *csrc; 95 | 96 | /** 97 | * (optional, presence indicated by Extension field) The first 32-bit word 98 | * contains a profile-specific identifier (16 bits) and a length specifier 99 | * (16 bits) that indicates the length of the extension in 32-bit units, 100 | * excluding the 32 bits of the extension header. The extension header data 101 | * follows. 102 | */ 103 | uint16_t extension_profile; 104 | 105 | /** 106 | * Indicates the length of the extension in 32-bit units. 107 | */ 108 | uint16_t extension_payload_len; 109 | 110 | /** 111 | * The pointer to extension header data. 112 | */ 113 | uint8_t *extension_payload; 114 | } SmolRTSP_RtpHeader; 115 | 116 | /** 117 | * Computes the size of the binary @p self. 118 | */ 119 | size_t SmolRTSP_RtpHeader_size(SmolRTSP_RtpHeader self) SMOLRTSP_PRIV_MUST_USE; 120 | 121 | /** 122 | * Writes @p self to @p buffer. 123 | * 124 | * @param[in] self The RTP header to write. 125 | * @param[out] buffer The pointer to write to. Must be at least of size 126 | * `SmolRTSP_RtpHeader_size(self)`. 127 | * 128 | * @return The pointer to a passed buffer. 129 | */ 130 | uint8_t *SmolRTSP_RtpHeader_serialize( 131 | SmolRTSP_RtpHeader self, uint8_t buffer[restrict]) SMOLRTSP_PRIV_MUST_USE; 132 | -------------------------------------------------------------------------------- /src/types/parsing.c: -------------------------------------------------------------------------------- 1 | #include "parsing.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | SmolRTSP_ParseResult smolrtsp_match_until( 8 | CharSlice99 input, bool (*matcher)(char c, void *ctx), void *ctx) { 9 | assert(matcher); 10 | 11 | size_t offset = 0; 12 | 13 | while (!CharSlice99_is_empty(input)) { 14 | if (!matcher(*(char *)input.ptr, ctx)) { 15 | return SmolRTSP_ParseResult_complete(offset); 16 | } 17 | 18 | input = CharSlice99_advance(input, 1); 19 | offset++; 20 | } 21 | 22 | return SmolRTSP_ParseResult_partial(); 23 | } 24 | 25 | static bool whitespace_matcher(char c, void *ctx) { 26 | (void)ctx; 27 | return c == ' '; 28 | } 29 | 30 | static bool non_whitespace_matcher(char c, void *ctx) { 31 | (void)ctx; 32 | return !whitespace_matcher(c, NULL); 33 | } 34 | 35 | static bool numeric_matcher(char c, void *ctx) { 36 | (void)ctx; 37 | return isdigit(c); 38 | } 39 | 40 | static bool ident_matcher(char c, void *ctx) { 41 | (void)ctx; 42 | return isalnum(c) || c == '_'; 43 | } 44 | 45 | static bool header_name_char_matcher(char c, void *ctx) { 46 | (void)ctx; 47 | return isalpha(c) || c == '-'; 48 | } 49 | 50 | static bool char_matcher(char c, void *ctx) { 51 | const char expected = *(const char *)ctx; 52 | return c == expected; 53 | } 54 | 55 | SmolRTSP_ParseResult 56 | smolrtsp_match_until_str(CharSlice99 input, const char *restrict str) { 57 | assert(str); 58 | 59 | const size_t str_len = strlen(str); 60 | assert(str_len > 0); 61 | 62 | size_t offset = 0; 63 | 64 | for (size_t i = 0; i < input.len; i++) { 65 | if (input.len - i < str_len) { 66 | return SmolRTSP_ParseResult_partial(); 67 | } 68 | 69 | if (memcmp(input.ptr + i, str, str_len) == 0) { 70 | return SmolRTSP_ParseResult_complete(offset + str_len); 71 | } 72 | 73 | offset++; 74 | } 75 | 76 | return SmolRTSP_ParseResult_partial(); 77 | } 78 | 79 | SmolRTSP_ParseResult smolrtsp_match_until_crlf(CharSlice99 input) { 80 | return smolrtsp_match_until_str(input, "\r\n"); 81 | } 82 | 83 | SmolRTSP_ParseResult smolrtsp_match_until_double_crlf(CharSlice99 input) { 84 | return smolrtsp_match_until_str(input, "\r\n\r\n"); 85 | } 86 | 87 | SmolRTSP_ParseResult smolrtsp_match_char(CharSlice99 input, char c) { 88 | return smolrtsp_match_until(input, char_matcher, &c); 89 | } 90 | 91 | SmolRTSP_ParseResult 92 | smolrtsp_match_str(CharSlice99 input, const char *restrict str) { 93 | assert(str); 94 | 95 | const size_t str_len = strlen(str); 96 | 97 | size_t offset = 0; 98 | 99 | const size_t min_len = input.len < str_len ? input.len : str_len; 100 | 101 | const bool are_coinciding = memcmp(input.ptr, str, min_len) == 0; 102 | if (!are_coinciding) { 103 | CharSlice99 expected = CharSlice99_from_str((char *)str), 104 | actual = CharSlice99_sub(input, 0, min_len); 105 | return SmolRTSP_ParseResult_Failure( 106 | SmolRTSP_ParseError_StrMismatch(expected, actual)); 107 | } 108 | 109 | if (input.len < str_len) { 110 | return SmolRTSP_ParseResult_partial(); 111 | } 112 | 113 | input = CharSlice99_advance(input, str_len); 114 | offset += str_len; 115 | return SmolRTSP_ParseResult_complete(offset); 116 | } 117 | 118 | SmolRTSP_ParseResult smolrtsp_match_whitespaces(CharSlice99 input) { 119 | return smolrtsp_match_until(input, whitespace_matcher, NULL); 120 | } 121 | 122 | SmolRTSP_ParseResult smolrtsp_match_non_whitespaces(CharSlice99 input) { 123 | return smolrtsp_match_until(input, non_whitespace_matcher, NULL); 124 | } 125 | 126 | // TODO: refactor the following functions. 127 | 128 | SmolRTSP_ParseResult smolrtsp_match_numeric(CharSlice99 input) { 129 | if (!numeric_matcher(input.ptr[0], NULL)) { 130 | return SmolRTSP_ParseResult_Failure( 131 | SmolRTSP_ParseError_TypeMismatch(SmolRTSP_ParseType_Int, input)); 132 | } 133 | 134 | return smolrtsp_match_until(input, numeric_matcher, NULL); 135 | } 136 | 137 | SmolRTSP_ParseResult smolrtsp_match_ident(CharSlice99 input) { 138 | if (!ident_matcher(input.ptr[0], NULL)) { 139 | return SmolRTSP_ParseResult_Failure( 140 | SmolRTSP_ParseError_TypeMismatch(SmolRTSP_ParseType_Ident, input)); 141 | } 142 | 143 | return smolrtsp_match_until(input, ident_matcher, NULL); 144 | } 145 | 146 | SmolRTSP_ParseResult smolrtsp_match_header_name(CharSlice99 input) { 147 | if (!header_name_char_matcher(input.ptr[0], NULL)) { 148 | return SmolRTSP_ParseResult_Failure(SmolRTSP_ParseError_TypeMismatch( 149 | SmolRTSP_ParseType_HeaderName, input)); 150 | } 151 | 152 | return smolrtsp_match_until(input, header_name_char_matcher, NULL); 153 | } 154 | -------------------------------------------------------------------------------- /include/smolrtsp/types/error.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief Possible parsing errors. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | /** 19 | * Types of data that can be failed to parse. 20 | */ 21 | typedef enum { 22 | /** 23 | * An integer (`-34`, `0`, `123`). 24 | */ 25 | SmolRTSP_ParseType_Int, 26 | 27 | /** 28 | * An identifier (`abc`). 29 | */ 30 | SmolRTSP_ParseType_Ident, 31 | 32 | /** 33 | * A header name (`Content-Length`, `Authorization`). 34 | */ 35 | SmolRTSP_ParseType_HeaderName, 36 | } SmolRTSP_ParseType; 37 | 38 | /** 39 | * Returns a string representation of @p self. 40 | */ 41 | const char * 42 | SmolRTSP_ParseType_str(SmolRTSP_ParseType self) SMOLRTSP_PRIV_MUST_USE; 43 | 44 | /** 45 | * An error that might occur during parsing. 46 | * 47 | * ## Variants 48 | * 49 | * - `ContentLength` -- An invalid value of the `Content-Length` header was 50 | * specified. Arguments: 51 | * 1. The value of this header. 52 | * - `StrMismatch` -- Two given strings are uneqal. Arguments: 53 | * 1. Expected string. 54 | * 2. Actual string. 55 | * - `TypeMismatch` -- Failed to parse an item. Arguments: 56 | * 1. A type of item failed to parse. 57 | * 2. The erroneous string. 58 | * - `HeaderMapOverflow` -- An attempt to add a header to a full header map. 59 | * - `MissingCSeq` -- Missing the `CSeq` header. 60 | * - `InvalidCSeq` -- Failed to parse the `CSeq` header. 61 | * 62 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 63 | */ 64 | 65 | // clang-format off 66 | datatype99( 67 | SmolRTSP_ParseError, 68 | (SmolRTSP_ParseError_ContentLength, CharSlice99), 69 | (SmolRTSP_ParseError_StrMismatch, CharSlice99, CharSlice99), 70 | (SmolRTSP_ParseError_TypeMismatch, SmolRTSP_ParseType, CharSlice99), 71 | (SmolRTSP_ParseError_HeaderMapOverflow), 72 | (SmolRTSP_ParseError_MissingCSeq), 73 | (SmolRTSP_ParseError_InvalidCSeq, CharSlice99) 74 | ); 75 | // clang-format on 76 | 77 | /** 78 | * Prints @p self into @p w. 79 | * 80 | * @param[in] self The error to print. 81 | * @param[in] w The writer to be provided with data. 82 | * 83 | * @return The number of bytes written or a negative value on error. 84 | * 85 | * @pre `w.self && w.vptr` 86 | */ 87 | int SmolRTSP_ParseError_print(SmolRTSP_ParseError self, SmolRTSP_Writer w) 88 | SMOLRTSP_PRIV_MUST_USE; 89 | 90 | /** 91 | * A status of successful parsing. 92 | * 93 | * ## Variants 94 | * 95 | * - `Complete` -- The parsing has completed. Arguments: 96 | * 1. A number of consumed bytes from the beginning of input. 97 | * - `Partial` -- Need more data to continue parsing. 98 | * 99 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 100 | */ 101 | 102 | // clang-format off 103 | datatype99( 104 | SmolRTSP_ParseStatus, 105 | (SmolRTSP_ParseStatus_Complete, size_t), 106 | (SmolRTSP_ParseStatus_Partial) 107 | ); 108 | // clang-format on 109 | 110 | /** 111 | * Returns whether @p self is complete. 112 | */ 113 | bool SmolRTSP_ParseStatus_is_complete(SmolRTSP_ParseStatus self) 114 | SMOLRTSP_PRIV_MUST_USE; 115 | 116 | /** 117 | * Returns whether @p self is partial. 118 | */ 119 | bool SmolRTSP_ParseStatus_is_partial(SmolRTSP_ParseStatus self) 120 | SMOLRTSP_PRIV_MUST_USE; 121 | 122 | /** 123 | * A result of parsing (either success or failure). 124 | * 125 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 126 | */ 127 | 128 | // clang-format off 129 | datatype99( 130 | SmolRTSP_ParseResult, 131 | (SmolRTSP_ParseResult_Success, SmolRTSP_ParseStatus), 132 | (SmolRTSP_ParseResult_Failure, SmolRTSP_ParseError) 133 | ); 134 | // clang-format on 135 | 136 | /** 137 | * Creates a **successful** and **partial** parse result. 138 | */ 139 | SmolRTSP_ParseResult SmolRTSP_ParseResult_partial(void) SMOLRTSP_PRIV_MUST_USE; 140 | 141 | /** 142 | * Creates a **successful** and **complete** parse result with the byte offset 143 | * @p offset (from the beginning of input). 144 | */ 145 | SmolRTSP_ParseResult 146 | SmolRTSP_ParseResult_complete(size_t offset) SMOLRTSP_PRIV_MUST_USE; 147 | 148 | /** 149 | * Returns whether @p self is successful. 150 | */ 151 | bool SmolRTSP_ParseResult_is_success(SmolRTSP_ParseResult self) 152 | SMOLRTSP_PRIV_MUST_USE; 153 | 154 | /** 155 | * Returns whether @p self is a failure. 156 | */ 157 | bool SmolRTSP_ParseResult_is_failure(SmolRTSP_ParseResult self) 158 | SMOLRTSP_PRIV_MUST_USE; 159 | 160 | /** 161 | * Returns whether @p self is both **successful** and **partial**. 162 | */ 163 | bool SmolRTSP_ParseResult_is_partial(SmolRTSP_ParseResult self) 164 | SMOLRTSP_PRIV_MUST_USE; 165 | 166 | /** 167 | * The same as #SmolRTSP_ParseResult_is_partial but for a complete result. 168 | */ 169 | bool SmolRTSP_ParseResult_is_complete(SmolRTSP_ParseResult self) 170 | SMOLRTSP_PRIV_MUST_USE; 171 | -------------------------------------------------------------------------------- /src/transport/udp.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #define MAX_RETRANSMITS 10 14 | 15 | typedef struct { 16 | int fd; 17 | char addr[128]; 18 | size_t len; 19 | } SmolRTSP_UdpTransport; 20 | 21 | declImpl(SmolRTSP_Transport, SmolRTSP_UdpTransport); 22 | 23 | static int send_packet(SmolRTSP_UdpTransport *self, struct msghdr message); 24 | static int 25 | new_sockaddr(struct sockaddr *addr, int af, const void *ip, uint16_t port); 26 | 27 | SmolRTSP_Transport smolrtsp_transport_udp(int fd) { 28 | assert(fd >= 0); 29 | 30 | SmolRTSP_UdpTransport *self = malloc(sizeof *self); 31 | assert(self); 32 | self->fd = fd; 33 | self->len = 0; 34 | 35 | return DYN(SmolRTSP_UdpTransport, SmolRTSP_Transport, self); 36 | } 37 | 38 | SmolRTSP_Transport 39 | smolrtsp_transport_udp_address(int fd, void *addr, size_t len) { 40 | assert(fd >= 0); 41 | assert(addr && len > 0); 42 | 43 | SmolRTSP_UdpTransport *self = malloc(sizeof *self); 44 | assert(self); 45 | self->fd = fd; 46 | self->len = len; 47 | memcpy(&self->addr, addr, len); 48 | 49 | return DYN(SmolRTSP_UdpTransport, SmolRTSP_Transport, self); 50 | } 51 | 52 | static void SmolRTSP_UdpTransport_drop(VSelf) { 53 | VSELF(SmolRTSP_UdpTransport); 54 | assert(self); 55 | 56 | free(self); 57 | } 58 | 59 | impl(SmolRTSP_Droppable, SmolRTSP_UdpTransport); 60 | 61 | static int SmolRTSP_UdpTransport_transmit(VSelf, SmolRTSP_IoVecSlice bufs) { 62 | VSELF(SmolRTSP_UdpTransport); 63 | assert(self); 64 | 65 | const struct msghdr msg = { 66 | .msg_name = self->addr, 67 | .msg_namelen = self->len, 68 | .msg_iov = bufs.ptr, 69 | .msg_iovlen = bufs.len, 70 | .msg_control = NULL, 71 | .msg_controllen = 0, 72 | .msg_flags = 0, 73 | }; 74 | 75 | return send_packet(self, msg); 76 | } 77 | 78 | static bool SmolRTSP_UdpTransport_is_full(VSelf) { 79 | VSELF(SmolRTSP_UdpTransport); 80 | (void)self; 81 | 82 | return false; 83 | } 84 | 85 | impl(SmolRTSP_Transport, SmolRTSP_UdpTransport); 86 | 87 | static int send_packet(SmolRTSP_UdpTransport *self, struct msghdr message) { 88 | // Try to retransmit a packet several times on `EMSGSIZE`. The kernel 89 | // will fragment an IP packet because if `IP_PMTUDISC_WANT` is set. 90 | size_t i = MAX_RETRANSMITS; 91 | do { 92 | const ssize_t ret = sendmsg(self->fd, &message, 0); 93 | 94 | if (ret != -1) { 95 | return 0; 96 | } 97 | if (EMSGSIZE != errno) { 98 | return -1; 99 | } 100 | 101 | assert(-1 == ret); 102 | assert(EMSGSIZE == errno); 103 | 104 | // Try to retransmit the packet one more time. 105 | i--; 106 | } while (i > 0); 107 | 108 | return -1; 109 | } 110 | 111 | int smolrtsp_dgram_socket(int af, const void *restrict addr, uint16_t port) { 112 | struct sockaddr_storage dest; 113 | memset(&dest, '\0', sizeof dest); 114 | if (new_sockaddr((struct sockaddr *)&dest, af, addr, port) == -1) { 115 | return -1; 116 | } 117 | 118 | int fd; 119 | if ((fd = socket(af, SOCK_DGRAM, 0)) == -1) { 120 | goto fail; 121 | } 122 | 123 | if (connect( 124 | fd, (const struct sockaddr *)&dest, 125 | AF_INET == af ? sizeof(struct sockaddr_in) 126 | : sizeof(struct sockaddr_in6)) == -1) { 127 | perror("connect"); 128 | goto fail; 129 | } 130 | 131 | const int enable_pmtud = 1; 132 | if (setsockopt( 133 | fd, IPPROTO_IP, IP_PMTUDISC_WANT, &enable_pmtud, 134 | sizeof enable_pmtud) == -1) { 135 | perror("setsockopt IP_PMTUDISC_WANT"); 136 | goto fail; 137 | } 138 | 139 | return fd; 140 | 141 | fail: 142 | close(fd); 143 | return -1; 144 | } 145 | 146 | static int 147 | new_sockaddr(struct sockaddr *addr, int af, const void *ip, uint16_t port) { 148 | switch (af) { 149 | case AF_INET: { 150 | struct sockaddr_in *addr_in = (struct sockaddr_in *)addr; 151 | addr_in->sin_family = AF_INET; 152 | memcpy(&addr_in->sin_addr, ip, sizeof(struct in_addr)); 153 | addr_in->sin_port = htons(port); 154 | return 0; 155 | } 156 | case AF_INET6: { 157 | struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr; 158 | addr_in6->sin6_family = AF_INET6; 159 | memcpy(&addr_in6->sin6_addr, ip, sizeof(struct in6_addr)); 160 | addr_in6->sin6_port = htons(port); 161 | return 0; 162 | } 163 | default: 164 | errno = EAFNOSUPPORT; 165 | return -1; 166 | } 167 | } 168 | 169 | void *smolrtsp_sockaddr_ip(const struct sockaddr *restrict addr) { 170 | assert(addr); 171 | 172 | switch (addr->sa_family) { 173 | case AF_INET: 174 | return (void *)&((struct sockaddr_in *)addr)->sin_addr; 175 | case AF_INET6: 176 | return (void *)&((struct sockaddr_in6 *)addr)->sin6_addr; 177 | default: 178 | return NULL; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /include/smolrtsp/nal.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief A generic [NAL (Network Abstraction 4 | * Layer)](https://en.wikipedia.org/wiki/Network_Abstraction_Layer) 5 | * representation. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | /** 22 | * A generic NAL header (either H.264 or H.265). 23 | * 24 | * See [Datatype99](https://github.com/hirrolot/datatype99) for the macro usage. 25 | */ 26 | 27 | // clang-format off 28 | datatype99( 29 | SmolRTSP_NalHeader, 30 | (SmolRTSP_NalHeader_H264, SmolRTSP_H264NalHeader), 31 | (SmolRTSP_NalHeader_H265, SmolRTSP_H265NalHeader) 32 | ); 33 | // clang-format on 34 | 35 | /** 36 | * Returns the NAL unit type of @p self. 37 | */ 38 | uint8_t 39 | SmolRTSP_NalHeader_unit_type(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 40 | 41 | /** 42 | * Computes the size of @p self in bytes. 43 | */ 44 | size_t SmolRTSP_NalHeader_size(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 45 | 46 | /** 47 | * Computes the size of a fragmentation unit (FU) header @p self. 48 | * 49 | * @see H.264 Fragmentation Units (FUs): 50 | * 51 | * @see H.265 Fragmentation Units (FUs): 52 | * 53 | */ 54 | size_t 55 | SmolRTSP_NalHeader_fu_size(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 56 | 57 | /** 58 | * Checks whether @p self is VPS. 59 | */ 60 | bool SmolRTSP_NalHeader_is_vps(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 61 | 62 | /** 63 | * Checks whether @p self is SPS. 64 | */ 65 | bool SmolRTSP_NalHeader_is_sps(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 66 | 67 | /** 68 | * Checks whether @p self is PPS. 69 | */ 70 | bool SmolRTSP_NalHeader_is_pps(SmolRTSP_NalHeader self) SMOLRTSP_PRIV_MUST_USE; 71 | 72 | /** 73 | * Checks whether @p self is a coded slice IDR. 74 | */ 75 | bool SmolRTSP_NalHeader_is_coded_slice_idr(SmolRTSP_NalHeader self) 76 | SMOLRTSP_PRIV_MUST_USE; 77 | 78 | /** 79 | * Checks whether @p self is a coded slice non-IDR. 80 | */ 81 | bool SmolRTSP_NalHeader_is_coded_slice_non_idr(SmolRTSP_NalHeader self) 82 | SMOLRTSP_PRIV_MUST_USE; 83 | 84 | /** 85 | * Writes the binary representation of @p self into @p buffer. 86 | * 87 | * @param[in] self The header to write. 88 | * @param[out] buffer The memory area capable of storing 89 | * `SmolRTSP_NalHeader_size(self)` bytes. 90 | */ 91 | void SmolRTSP_NalHeader_serialize( 92 | SmolRTSP_NalHeader self, uint8_t buffer[restrict]); 93 | 94 | /** 95 | * Writes a FU header of @p self to @p buffer. 96 | * 97 | * @param[in] self The header to write. 98 | * @param[out] buffer The memory area capable of storing 99 | * `SmolRTSP_NalHeader_fu_size(self)` bytes. 100 | * @param[in] is_first_fragment The indication of a start of the FU. 101 | * @param[in] is_last_fragment The indication of an end of the FU. 102 | * 103 | * @see H.264 Fragmentation Units (FUs): 104 | * 105 | * @see H.265 Fragmentation Units (FUs): 106 | * 107 | */ 108 | void SmolRTSP_NalHeader_write_fu_header( 109 | SmolRTSP_NalHeader self, uint8_t buffer[restrict], bool is_first_fragment, 110 | bool is_last_fragment); 111 | 112 | /** 113 | * A generic NAL unit (either H.264 or H.265). 114 | */ 115 | typedef struct { 116 | /** 117 | * The header of this NAL unit. 118 | */ 119 | SmolRTSP_NalHeader header; 120 | 121 | /** 122 | * The payload data of this NAL unit (not including the header). 123 | */ 124 | U8Slice99 payload; 125 | } SmolRTSP_NalUnit; 126 | 127 | /** 128 | * Creates a generic NAL FU header. 129 | */ 130 | uint8_t smolrtsp_nal_fu_header( 131 | bool is_first_fragment, bool is_last_fragment, 132 | uint8_t unit_type) SMOLRTSP_PRIV_MUST_USE; 133 | 134 | /** 135 | * A function that tests whether @p data starts with some start code. 136 | * 137 | * @return The number of start code bytes. If there is no start code in the 138 | * beginning of @p data, returns 0. 139 | * 140 | * @see #smolrtsp_test_start_code_3b 141 | * @see #smolrtsp_test_start_code_4b 142 | */ 143 | typedef size_t (*SmolRTSP_NalStartCodeTester)(U8Slice99 data); 144 | 145 | /** 146 | * Returns a start code tester for @p data. 147 | * 148 | * If @p data does not begin with a start code (either `0x000001` or 149 | * `0x00000001`), returns `NULL`. 150 | * 151 | * This function is useful when you have some `*.h264` bitstream file and you 152 | * want to determine what start code it uses -- just pass the beginning of this 153 | * file to #smolrtsp_determine_start_code and invoke the returned tester 154 | * multiple times afterwards. 155 | */ 156 | SmolRTSP_NalStartCodeTester 157 | smolrtsp_determine_start_code(U8Slice99 data) SMOLRTSP_PRIV_MUST_USE; 158 | 159 | /** 160 | * The 3-byte start code tester (`0x000001`). 161 | */ 162 | size_t smolrtsp_test_start_code_3b(U8Slice99 data); 163 | 164 | /** 165 | * The 3-byte start code tester (`0x00000001`). 166 | */ 167 | size_t smolrtsp_test_start_code_4b(U8Slice99 data); 168 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | const char *SmolRTSP_LowerTransport_str(SmolRTSP_LowerTransport self) { 13 | switch (self) { 14 | case SmolRTSP_LowerTransport_TCP: 15 | return "TCP"; 16 | case SmolRTSP_LowerTransport_UDP: 17 | return "UDP"; 18 | default: 19 | return "Unknown"; 20 | } 21 | } 22 | 23 | #define PARSE_RANGE(lhs, rhs, specifier, param) \ 24 | sscanf((param), "%*[^=]=%" specifier "-%" specifier, &(lhs), &(rhs)) 25 | #define STARTS_WITH(str, beginning) \ 26 | (strncmp((str), (beginning), strlen(beginning)) == 0) 27 | 28 | static int parse_lower_transport( 29 | SmolRTSP_TransportConfig *restrict result, const char *header_value); 30 | static int parse_transport_param( 31 | SmolRTSP_TransportConfig *restrict result, const char *param); 32 | static int parse_channel_pair( 33 | SmolRTSP_ChannelPair *restrict val, const char *restrict param); 34 | static int 35 | parse_port_pair(SmolRTSP_PortPair *restrict val, const char *restrict param); 36 | 37 | int smolrtsp_parse_transport( 38 | SmolRTSP_TransportConfig *restrict config, CharSlice99 header_value) { 39 | assert(config); 40 | 41 | SmolRTSP_TransportConfig result = { 42 | .lower = 0, 43 | .unicast = false, 44 | .multicast = false, 45 | .interleaved = SmolRTSP_ChannelPair_None(), 46 | .client_port = SmolRTSP_PortPair_None(), 47 | }; 48 | 49 | const char *input = CharSlice99_alloca_c_str(header_value); 50 | 51 | if (parse_lower_transport(&result, input) == -1) { 52 | return -1; 53 | } 54 | if ((input = strchr(input, ';')) == NULL) { 55 | goto success; 56 | } 57 | input++; 58 | 59 | while (input != NULL) { 60 | const char *next_param = strchr(input, ';'); 61 | const CharSlice99 param = 62 | NULL == next_param 63 | ? CharSlice99_from_str((char *)input) 64 | : CharSlice99_from_ptrdiff((char *)input, (char *)next_param); 65 | 66 | if (parse_transport_param(&result, CharSlice99_alloca_c_str(param)) == 67 | -1) { 68 | return -1; 69 | } 70 | 71 | input = next_param != NULL ? next_param + 1 : NULL; 72 | } 73 | 74 | success: 75 | *config = result; 76 | return 0; 77 | } 78 | 79 | static int parse_lower_transport( 80 | SmolRTSP_TransportConfig *restrict result, const char *header_value) { 81 | const char *rtp_avp = "RTP/AVP"; 82 | if (strncmp(header_value, rtp_avp, strlen(rtp_avp)) != 0) { 83 | return -1; 84 | } 85 | header_value += strlen(rtp_avp); 86 | 87 | if (strncmp(header_value, "/TCP", strlen("/TCP")) == 0) { 88 | result->lower = SmolRTSP_LowerTransport_TCP; 89 | } else if (strncmp(header_value, "/UDP", strlen("/UDP")) == 0) { 90 | result->lower = SmolRTSP_LowerTransport_UDP; 91 | } else { 92 | result->lower = SmolRTSP_LowerTransport_UDP; 93 | } 94 | 95 | return 0; 96 | } 97 | 98 | static int parse_transport_param( 99 | SmolRTSP_TransportConfig *restrict result, const char *param) { 100 | if (STARTS_WITH(param, "unicast")) { 101 | result->unicast = true; 102 | } else if (STARTS_WITH(param, "multicast")) { 103 | result->multicast = true; 104 | } else if (STARTS_WITH(param, "interleaved")) { 105 | SmolRTSP_ChannelPair val; 106 | if (parse_channel_pair(&val, param) == -1) { 107 | return -1; 108 | } 109 | result->interleaved = SmolRTSP_ChannelPair_Some(val); 110 | } else if (STARTS_WITH(param, "client_port")) { 111 | SmolRTSP_PortPair val; 112 | if (parse_port_pair(&val, param) == -1) { 113 | return -1; 114 | } 115 | result->client_port = SmolRTSP_PortPair_Some(val); 116 | } 117 | 118 | return 0; 119 | } 120 | 121 | static int parse_channel_pair( 122 | SmolRTSP_ChannelPair *restrict val, const char *restrict param) { 123 | if (PARSE_RANGE(val->rtp_channel, val->rtcp_channel, SCNu8, param) < 1) { 124 | return -1; 125 | } 126 | 127 | return 0; 128 | } 129 | 130 | static int 131 | parse_port_pair(SmolRTSP_PortPair *restrict val, const char *restrict param) { 132 | if (PARSE_RANGE(val->rtp_port, val->rtcp_port, SCNu16, param) < 1) { 133 | return -1; 134 | } 135 | 136 | return 0; 137 | } 138 | 139 | uint32_t smolrtsp_interleaved_header(uint8_t channel_id, uint16_t payload_len) { 140 | uint8_t bytes[sizeof(uint32_t)] = {0}; 141 | 142 | bytes[0] = '$'; 143 | bytes[1] = channel_id; 144 | memcpy(bytes + 2, &payload_len, sizeof payload_len); 145 | 146 | uint32_t n; 147 | memcpy(&n, bytes, sizeof bytes); 148 | return n; 149 | } 150 | 151 | void smolrtsp_parse_interleaved_header( 152 | const uint8_t data[restrict static 4], uint8_t *restrict channel_id, 153 | uint16_t *restrict payload_len) { 154 | assert(channel_id); 155 | assert(payload_len); 156 | assert('$' == data[0]); 157 | 158 | *channel_id = data[1]; 159 | memcpy(payload_len, data + 2, sizeof *payload_len); 160 | *payload_len = ntohs(*payload_len); 161 | } 162 | -------------------------------------------------------------------------------- /src/nal_transport.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | static int send_fragmentized_nal_data( 12 | SmolRTSP_RtpTransport *t, SmolRTSP_RtpTimestamp ts, size_t max_packet_size, 13 | SmolRTSP_NalUnit nalu); 14 | static int send_fu( 15 | SmolRTSP_RtpTransport *t, SmolRTSP_RtpTimestamp ts, SmolRTSP_NalUnit fu, 16 | bool is_first_fragment, bool is_last_fragment); 17 | 18 | SmolRTSP_NalTransportConfig SmolRTSP_NalTransportConfig_default(void) { 19 | return (SmolRTSP_NalTransportConfig){ 20 | .max_h264_nalu_size = SMOLRTSP_MAX_H264_NALU_SIZE, 21 | .max_h265_nalu_size = SMOLRTSP_MAX_H265_NALU_SIZE, 22 | }; 23 | } 24 | 25 | struct SmolRTSP_NalTransport { 26 | SmolRTSP_RtpTransport *transport; 27 | SmolRTSP_NalTransportConfig config; 28 | }; 29 | 30 | SmolRTSP_NalTransport *SmolRTSP_NalTransport_new(SmolRTSP_RtpTransport *t) { 31 | assert(t); 32 | 33 | return SmolRTSP_NalTransport_new_with_config( 34 | t, SmolRTSP_NalTransportConfig_default()); 35 | } 36 | 37 | SmolRTSP_NalTransport *SmolRTSP_NalTransport_new_with_config( 38 | SmolRTSP_RtpTransport *t, SmolRTSP_NalTransportConfig config) { 39 | assert(t); 40 | 41 | SmolRTSP_NalTransport *self = malloc(sizeof *self); 42 | assert(self); 43 | 44 | self->transport = t; 45 | self->config = config; 46 | 47 | return self; 48 | } 49 | 50 | static void SmolRTSP_NalTransport_drop(VSelf) { 51 | VSELF(SmolRTSP_NalTransport); 52 | assert(self); 53 | 54 | VTABLE(SmolRTSP_RtpTransport, SmolRTSP_Droppable).drop(self->transport); 55 | 56 | free(self); 57 | } 58 | 59 | implExtern(SmolRTSP_Droppable, SmolRTSP_NalTransport); 60 | 61 | bool SmolRTSP_NalTransport_is_full(SmolRTSP_NalTransport *self) { 62 | return SmolRTSP_RtpTransport_is_full(self->transport); 63 | } 64 | 65 | int SmolRTSP_NalTransport_send_packet( 66 | SmolRTSP_NalTransport *self, SmolRTSP_RtpTimestamp ts, 67 | SmolRTSP_NalUnit nalu) { 68 | assert(self); 69 | 70 | const size_t max_packet_size = MATCHES(nalu.header, SmolRTSP_NalHeader_H264) 71 | ? self->config.max_h264_nalu_size 72 | : self->config.max_h265_nalu_size, 73 | nalu_size = 74 | SmolRTSP_NalHeader_size(nalu.header) + nalu.payload.len; 75 | 76 | if (nalu_size < max_packet_size) { 77 | const bool marker = 78 | SmolRTSP_NalHeader_is_coded_slice_idr(nalu.header) || 79 | SmolRTSP_NalHeader_is_coded_slice_non_idr(nalu.header); 80 | 81 | const size_t header_buf_size = SmolRTSP_NalHeader_size(nalu.header); 82 | uint8_t *header_buf = alloca(header_buf_size); 83 | SmolRTSP_NalHeader_serialize(nalu.header, header_buf); 84 | 85 | return SmolRTSP_RtpTransport_send_packet( 86 | self->transport, ts, marker, 87 | U8Slice99_new(header_buf, header_buf_size), nalu.payload); 88 | } 89 | 90 | return send_fragmentized_nal_data( 91 | self->transport, ts, max_packet_size, nalu); 92 | } 93 | 94 | // See (H.264), 95 | // (H.265). 96 | static int send_fragmentized_nal_data( 97 | SmolRTSP_RtpTransport *t, SmolRTSP_RtpTimestamp ts, size_t max_packet_size, 98 | SmolRTSP_NalUnit nalu) { 99 | const size_t rem = nalu.payload.len % max_packet_size, 100 | packets_count = (nalu.payload.len - rem) / max_packet_size; 101 | 102 | for (size_t packet_idx = 0; packet_idx < packets_count; packet_idx++) { 103 | const bool is_first_fragment = 0 == packet_idx, 104 | is_last_fragment = 105 | 0 == rem && (packets_count - 1 == packet_idx); 106 | 107 | const U8Slice99 fu_data = U8Slice99_sub( 108 | nalu.payload, packet_idx * max_packet_size, 109 | is_last_fragment ? nalu.payload.len 110 | : (packet_idx + 1) * max_packet_size); 111 | const SmolRTSP_NalUnit fu = {nalu.header, fu_data}; 112 | 113 | if (send_fu(t, ts, fu, is_first_fragment, is_last_fragment) == -1) { 114 | return -1; 115 | } 116 | } 117 | 118 | if (rem != 0) { 119 | const U8Slice99 fu_data = 120 | U8Slice99_advance(nalu.payload, packets_count * max_packet_size); 121 | const SmolRTSP_NalUnit fu = {nalu.header, fu_data}; 122 | const bool is_first_fragment = 0 == packets_count, 123 | is_last_fragment = true; 124 | 125 | if (send_fu(t, ts, fu, is_first_fragment, is_last_fragment) == -1) { 126 | return -1; 127 | } 128 | } 129 | 130 | return 0; 131 | } 132 | 133 | static int send_fu( 134 | SmolRTSP_RtpTransport *t, SmolRTSP_RtpTimestamp ts, SmolRTSP_NalUnit fu, 135 | bool is_first_fragment, bool is_last_fragment) { 136 | const size_t fu_header_size = SmolRTSP_NalHeader_fu_size(fu.header); 137 | U8Slice99 fu_header = U8Slice99_new(alloca(fu_header_size), fu_header_size); 138 | 139 | SmolRTSP_NalHeader_write_fu_header( 140 | fu.header, fu_header.ptr, is_first_fragment, is_last_fragment); 141 | 142 | const bool marker = is_last_fragment; 143 | 144 | return SmolRTSP_RtpTransport_send_packet( 145 | t, ts, marker, fu_header, fu.payload); 146 | } 147 | -------------------------------------------------------------------------------- /include/smolrtsp/writer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @brief The writer interface. 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | /** 20 | * The user-supplied data writer interface. 21 | */ 22 | #define SmolRTSP_Writer_IFACE \ 23 | \ 24 | /* \ 25 | * Writes @p data into itself. \ 26 | * \ 27 | * @param[in] data The slice of character data to write. \ 28 | * \ 29 | * @return The number of bytes written or a negative value on error. \ 30 | */ \ 31 | vfunc99(ssize_t, write, VSelf99, CharSlice99 data) \ 32 | \ 33 | /* \ 34 | * Lock writer to prevent race conditions on TCP interleaved channels \ 35 | */ \ 36 | vfunc99(void, lock, VSelf99) \ 37 | \ 38 | /* \ 39 | * Unlock writer locked by `lock` \ 40 | */ \ 41 | vfunc99(void, unlock, VSelf99) \ 42 | \ 43 | /* \ 44 | * Get current size of output buffer \ 45 | */ \ 46 | vfunc99(size_t, filled, VSelf99) \ 47 | \ 48 | /* \ 49 | * Writes a formatted string into itself. \ 50 | * \ 51 | * @param[in] fmt The `printf`-like format string. \ 52 | * \ 53 | * @return The number of bytes written or a negative value on error. \ 54 | */ \ 55 | vfunc99(int, writef, VSelf99, const char *restrict fmt, ...) \ 56 | \ 57 | /* \ 58 | * The same as `printf` but accepts `va_list`. \ 59 | */ \ 60 | vfunc99(int, vwritef, VSelf99, const char *restrict fmt, va_list ap) 61 | 62 | /** 63 | * Defines the `SmolRTSP_Writer` interface. 64 | * 65 | * See [Interface99](https://github.com/hirrolot/interface99) for the macro 66 | * usage. 67 | */ 68 | interface99(SmolRTSP_Writer); 69 | 70 | /** 71 | * The same as #smolrtsp_write_slices but calculates an array length from 72 | * variadic arguments (the syntactically separated items of the array). 73 | */ 74 | #define SMOLRTSP_WRITE_SLICES(w, ...) \ 75 | smolrtsp_write_slices( \ 76 | w, SLICE99_ARRAY_LEN((const CharSlice99[])__VA_ARGS__), \ 77 | (const CharSlice99[])__VA_ARGS__) 78 | 79 | /** 80 | * Sequentially writes all items in @p data to @p w. 81 | * 82 | * @param[in] w The writer to be provided with data. 83 | * @param[in] len The number of items in @p data. 84 | * @param[in] data The data array which will be written to @p w, one-by-one. 85 | * 86 | * @return The number of bytes written or a negative value on error. 87 | * 88 | * @pre `w.self && w.vptr` 89 | */ 90 | ssize_t smolrtsp_write_slices( 91 | SmolRTSP_Writer w, size_t len, 92 | const CharSlice99 data[restrict static len]) SMOLRTSP_PRIV_MUST_USE; 93 | 94 | /** 95 | * A writer that invokes `write` on a provided file descriptor. 96 | * 97 | * @pre `fd != NULL` 98 | */ 99 | SmolRTSP_Writer smolrtsp_fd_writer(int *fd) SMOLRTSP_PRIV_MUST_USE; 100 | 101 | /** 102 | * A writer that invokes `fwrite` on a provided file pointer. 103 | * 104 | * @pre `stream != NULL` 105 | */ 106 | SmolRTSP_Writer smolrtsp_file_writer(FILE *stream) SMOLRTSP_PRIV_MUST_USE; 107 | 108 | /** 109 | * A writer that invokes `strncat` on a provided buffer. 110 | * 111 | * @pre @p buffer shall be capable of holding all characters that will be 112 | * written into it. 113 | */ 114 | SmolRTSP_Writer smolrtsp_string_writer(char *buffer) SMOLRTSP_PRIV_MUST_USE; 115 | -------------------------------------------------------------------------------- /src/nal.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #define NAL_HEADER_DERIVE_GETTER(T, name, h264_value, h265_value) \ 6 | T SmolRTSP_NalHeader_##name(SmolRTSP_NalHeader self) { \ 7 | T result = 0; \ 8 | \ 9 | match(self) { \ 10 | of(SmolRTSP_NalHeader_H264, h) result = h264_value; \ 11 | of(SmolRTSP_NalHeader_H265, h) result = h265_value; \ 12 | } \ 13 | \ 14 | return result; \ 15 | } 16 | 17 | NAL_HEADER_DERIVE_GETTER(uint8_t, unit_type, h->unit_type, h->unit_type) 18 | NAL_HEADER_DERIVE_GETTER( 19 | size_t, size, ((void)h, SMOLRTSP_H264_NAL_HEADER_SIZE), 20 | ((void)h, SMOLRTSP_H265_NAL_HEADER_SIZE)) 21 | NAL_HEADER_DERIVE_GETTER( 22 | size_t, fu_size, ((void)h, SMOLRTSP_H264_FU_HEADER_SIZE), 23 | ((void)h, SMOLRTSP_H265_FU_HEADER_SIZE)) 24 | 25 | #undef NAL_HEADER_DERIVE_GETTER 26 | 27 | #define NAL_HEADER_DERIVE_PREDICATE(fn) \ 28 | bool SmolRTSP_NalHeader_##fn(SmolRTSP_NalHeader self) { \ 29 | bool result = 0; \ 30 | \ 31 | match(self) { \ 32 | of(SmolRTSP_NalHeader_H264, h) result = \ 33 | SmolRTSP_H264NalHeader_##fn(*h); \ 34 | of(SmolRTSP_NalHeader_H265, h) result = \ 35 | SmolRTSP_H265NalHeader_##fn(*h); \ 36 | } \ 37 | \ 38 | return result; \ 39 | } 40 | 41 | NAL_HEADER_DERIVE_PREDICATE(is_vps) 42 | NAL_HEADER_DERIVE_PREDICATE(is_sps) 43 | NAL_HEADER_DERIVE_PREDICATE(is_pps) 44 | NAL_HEADER_DERIVE_PREDICATE(is_coded_slice_idr) 45 | NAL_HEADER_DERIVE_PREDICATE(is_coded_slice_non_idr) 46 | 47 | #undef NAL_HEADER_DERIVE_PREDICATE 48 | 49 | void SmolRTSP_NalHeader_serialize( 50 | SmolRTSP_NalHeader self, uint8_t buffer[restrict]) { 51 | match(self) { 52 | of(SmolRTSP_NalHeader_H264, h) { 53 | const uint8_t repr = SmolRTSP_H264NalHeader_serialize(*h); 54 | buffer[0] = repr; 55 | } 56 | of(SmolRTSP_NalHeader_H265, h) { 57 | const uint16_t repr = SmolRTSP_H265NalHeader_serialize(*h); 58 | memcpy(buffer, &repr, sizeof repr); 59 | } 60 | } 61 | } 62 | 63 | void SmolRTSP_NalHeader_write_fu_header( 64 | SmolRTSP_NalHeader self, uint8_t buffer[restrict], bool is_first_fragment, 65 | bool is_last_fragment) { 66 | match(self) { 67 | of(SmolRTSP_NalHeader_H264, h) SmolRTSP_H264NalHeader_write_fu_header( 68 | *h, buffer, is_first_fragment, is_last_fragment); 69 | of(SmolRTSP_NalHeader_H265, h) SmolRTSP_H265NalHeader_write_fu_header( 70 | *h, buffer, is_first_fragment, is_last_fragment); 71 | } 72 | } 73 | 74 | // See (H.265), 75 | // (H.264). 76 | uint8_t smolrtsp_nal_fu_header( 77 | bool is_first_fragment, bool is_last_fragment, uint8_t unit_type) { 78 | /* 79 | * H.264: 80 | * +---------------+ 81 | * |0|1|2|3|4|5|6|7| 82 | * +-+-+-+-+-+-+-+-+ 83 | * |S|E|R| Type | 84 | * +---------------+ 85 | * */ 86 | 87 | /* 88 | * H.265: 89 | * +---------------+ 90 | * |0|1|2|3|4|5|6|7| 91 | * +-+-+-+-+-+-+-+-+ 92 | * |S|E| FuType | 93 | * +---------------+ 94 | */ 95 | uint8_t fu_header = (uint8_t)0b00000000; 96 | 97 | if (is_first_fragment) { 98 | fu_header |= (uint8_t)0b10000000; 99 | } 100 | if (is_last_fragment) { 101 | fu_header |= (uint8_t)0b01000000; 102 | } 103 | fu_header += unit_type; 104 | 105 | return fu_header; 106 | } 107 | 108 | static const uint8_t start_code_3b[] = {0x00, 0x00, 0x01}, 109 | start_code_4b[] = {0x00, 0x00, 0x00, 0x01}; 110 | 111 | SmolRTSP_NalStartCodeTester smolrtsp_determine_start_code(U8Slice99 data) { 112 | if (smolrtsp_test_start_code_3b(data)) { 113 | return smolrtsp_test_start_code_3b; 114 | } 115 | 116 | if (smolrtsp_test_start_code_4b(data)) { 117 | return smolrtsp_test_start_code_4b; 118 | } 119 | 120 | return NULL; 121 | } 122 | 123 | size_t smolrtsp_test_start_code_3b(U8Slice99 data) { 124 | if (data.len < 3) { 125 | return 0; 126 | } 127 | if (memcmp(data.ptr, start_code_3b, 3) == 0) { 128 | return 3; 129 | } 130 | 131 | return 0; 132 | } 133 | 134 | size_t smolrtsp_test_start_code_4b(U8Slice99 data) { 135 | if (data.len < 4) { 136 | return 0; 137 | } 138 | if (memcmp(data.ptr, start_code_4b, 4) == 0) { 139 | return 4; 140 | } 141 | 142 | return 0; 143 | } 144 | --------------------------------------------------------------------------------