├── .dockerignore ├── .github ├── DISCUSSION_TEMPLATE │ └── questions.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── dependabot.yml └── workflows │ ├── code_lint.yml │ ├── code_test.yml │ ├── issue_lock.yml │ ├── nightly_binaries.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── apidocs └── openapi.yaml ├── docker ├── ffmpeg-rpi.Dockerfile ├── ffmpeg.Dockerfile ├── rpi.Dockerfile └── standard.Dockerfile ├── go.mod ├── go.sum ├── internal ├── api │ ├── api.go │ ├── api_test.go │ ├── paginate.go │ ├── paginate_test.go │ └── testdata │ │ └── fuzz │ │ └── FuzzPaginate │ │ ├── 23731da0f18d31d0 │ │ ├── 34523a772174e26e │ │ └── 85649d45641911d0 ├── auth │ ├── credentials.go │ ├── jwt_claims.go │ ├── manager.go │ ├── manager_test.go │ └── request.go ├── certloader │ ├── certloader.go │ └── certloader_test.go ├── conf │ ├── auth_action.go │ ├── auth_internal_users.go │ ├── auth_method.go │ ├── conf.go │ ├── conf_test.go │ ├── credential.go │ ├── credential_test.go │ ├── decrypt │ │ └── decrypt.go │ ├── duration.go │ ├── duration_test.go │ ├── encryption.go │ ├── env │ │ ├── env.go │ │ └── env_test.go │ ├── global.go │ ├── hls_variant.go │ ├── ip_networks.go │ ├── jsonwrapper │ │ └── unmarshal.go │ ├── log_destination.go │ ├── log_level.go │ ├── optional_global.go │ ├── optional_path.go │ ├── path.go │ ├── record_format.go │ ├── rtsp_auth_methods.go │ ├── rtsp_range_type.go │ ├── rtsp_transport.go │ ├── rtsp_transports.go │ ├── string_size.go │ ├── webrtc_ice_server.go │ └── yamlwrapper │ │ └── unmarshal.go ├── confwatcher │ ├── confwatcher.go │ └── confwatcher_test.go ├── core │ ├── api_test.go │ ├── core.go │ ├── core_test.go │ ├── metrics_test.go │ ├── path.go │ ├── path_manager.go │ ├── path_manager_test.go │ ├── path_test.go │ ├── source_redirect.go │ ├── test_on_demand │ │ └── main.go │ └── versiongetter │ │ └── main.go ├── counterdumper │ ├── counterdumper.go │ └── counterdumper_test.go ├── defs │ ├── api.go │ ├── defs.go │ ├── path.go │ ├── path_access_request.go │ ├── publisher.go │ ├── reader.go │ ├── source.go │ └── static_source.go ├── externalcmd │ ├── cmd.go │ ├── cmd_unix.go │ ├── cmd_win.go │ └── pool.go ├── formatprocessor │ ├── ac3.go │ ├── av1.go │ ├── g711.go │ ├── g711_test.go │ ├── generic.go │ ├── generic_test.go │ ├── h264.go │ ├── h264_test.go │ ├── h265.go │ ├── h265_test.go │ ├── lpcm.go │ ├── lpcm_test.go │ ├── mjpeg.go │ ├── mpeg1_audio.go │ ├── mpeg1_video.go │ ├── mpeg4_audio.go │ ├── mpeg4_video.go │ ├── opus.go │ ├── opus_test.go │ ├── processor.go │ ├── processor_test.go │ ├── testdata │ │ └── fuzz │ │ │ ├── FuzzRTPH264ExtractSPSPPS │ │ │ ├── 048b606517c23baf │ │ │ ├── 32e7782636603e29 │ │ │ ├── caf81e9797b19c76 │ │ │ └── f428976a5b2917c0 │ │ │ └── FuzzRTPH265ExtractParams │ │ │ ├── 353ba911ad2dc191 │ │ │ ├── 3c3a72c00adac0b3 │ │ │ ├── 582528ddfad69eb5 │ │ │ └── c4389a565e828050 │ ├── vp8.go │ └── vp9.go ├── hooks │ ├── hooks.go │ ├── on_connect.go │ ├── on_demand.go │ ├── on_init.go │ ├── on_read.go │ └── on_ready.go ├── logger │ ├── destination.go │ ├── destination_file.go │ ├── destination_stdout.go │ ├── destination_syslog.go │ ├── level.go │ ├── logger.go │ ├── syslog_unix.go │ ├── syslog_win.go │ └── writer.go ├── metrics │ ├── metrics.go │ └── metrics_test.go ├── playback │ ├── muxer.go │ ├── muxer_fmp4.go │ ├── muxer_mp4.go │ ├── on_get.go │ ├── on_get_test.go │ ├── on_list.go │ ├── on_list_test.go │ ├── segment_fmp4.go │ ├── segment_fmp4_test.go │ ├── server.go │ └── server_test.go ├── pprof │ ├── pprof.go │ └── pprof_test.go ├── protocols │ ├── hls │ │ ├── from_stream.go │ │ ├── from_stream_test.go │ │ ├── to_stream.go │ │ └── to_stream_test.go │ ├── httpp │ │ ├── content_type.go │ │ ├── credentials.go │ │ ├── credentials_test.go │ │ ├── handler_exit_on_panic.go │ │ ├── handler_filter_requests.go │ │ ├── handler_logger.go │ │ ├── handler_server_header.go │ │ ├── remote_addr.go │ │ ├── server.go │ │ └── server_test.go │ ├── mpegts │ │ ├── from_stream.go │ │ ├── from_stream_test.go │ │ ├── to_stream.go │ │ └── to_stream_test.go │ ├── rtmp │ │ ├── amf0 │ │ │ ├── data.go │ │ │ ├── data_test.go │ │ │ ├── object.go │ │ │ ├── object_test.go │ │ │ └── testdata │ │ │ │ └── fuzz │ │ │ │ └── FuzzUnmarshal │ │ │ │ ├── 110121ffaa6941a6 │ │ │ │ ├── 118a6dec0931d635 │ │ │ │ ├── 13ba6ce8ecfbc991 │ │ │ │ ├── 399a2041db7e1ff9 │ │ │ │ ├── 582528ddfad69eb5 │ │ │ │ ├── 6df12bd1a096b953 │ │ │ │ ├── 6f9c29532ccb80bf │ │ │ │ ├── 7fbe967ee430d9f4 │ │ │ │ ├── 97dc7172b48e6ffd │ │ │ │ ├── aba8f2fdaa5caedb │ │ │ │ ├── b27b930eec286237 │ │ │ │ ├── b9ef082bd4d297b2 │ │ │ │ ├── de35ed0e8078c6ef │ │ │ │ ├── df015416c2cf2dc6 │ │ │ │ ├── e93e775e4de86c93 │ │ │ │ ├── f3b49d384cddc291 │ │ │ │ └── fb46839f39edbbd8 │ │ ├── bytecounter │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── readwriter.go │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── chunk │ │ │ ├── chunk.go │ │ │ ├── chunk0.go │ │ │ ├── chunk1.go │ │ │ ├── chunk2.go │ │ │ ├── chunk3.go │ │ │ ├── chunk_test.go │ │ │ └── testdata │ │ │ │ └── fuzz │ │ │ │ ├── FuzzChunk0Read │ │ │ │ ├── 582528ddfad69eb5 │ │ │ │ └── 5f73a77c7f93e5f8 │ │ │ │ ├── FuzzChunk1Read │ │ │ │ ├── 553384c8664fe971 │ │ │ │ └── 582528ddfad69eb5 │ │ │ │ ├── FuzzChunk2Read │ │ │ │ ├── 582528ddfad69eb5 │ │ │ │ └── feb2b2a8b4ba63ba │ │ │ │ └── FuzzChunk3Read │ │ │ │ ├── 582528ddfad69eb5 │ │ │ │ └── caf81e9797b19c76 │ │ ├── client.go │ │ ├── client_test.go │ │ ├── conn.go │ │ ├── from_stream.go │ │ ├── from_stream_test.go │ │ ├── h264conf │ │ │ ├── h264conf.go │ │ │ └── h264conf_test.go │ │ ├── handshake │ │ │ ├── c0s0.go │ │ │ ├── c0s0_test.go │ │ │ ├── c1s1.go │ │ │ ├── c1s1_test.go │ │ │ ├── c2s2.go │ │ │ ├── c2s2_test.go │ │ │ ├── dh.go │ │ │ ├── handshake.go │ │ │ └── handshake_test.go │ │ ├── message │ │ │ ├── message.go │ │ │ ├── msg_acknowledge.go │ │ │ ├── msg_audio.go │ │ │ ├── msg_audio_ex_coded_frames.go │ │ │ ├── msg_audio_ex_multichannel_config.go │ │ │ ├── msg_audio_ex_multitrack.go │ │ │ ├── msg_audio_ex_sequence_end.go │ │ │ ├── msg_audio_ex_sequence_start.go │ │ │ ├── msg_command_amf0.go │ │ │ ├── msg_command_amf0_test.go │ │ │ ├── msg_data_amf0.go │ │ │ ├── msg_set_chunk_size.go │ │ │ ├── msg_set_peer_bandwidth.go │ │ │ ├── msg_set_window_ack_size.go │ │ │ ├── msg_user_control_ping_request.go │ │ │ ├── msg_user_control_ping_response.go │ │ │ ├── msg_user_control_set_buffer_length.go │ │ │ ├── msg_user_control_stream_begin.go │ │ │ ├── msg_user_control_stream_dry.go │ │ │ ├── msg_user_control_stream_eof.go │ │ │ ├── msg_user_control_stream_is_recorded.go │ │ │ ├── msg_video.go │ │ │ ├── msg_video_ex_coded_frames.go │ │ │ ├── msg_video_ex_frames_x.go │ │ │ ├── msg_video_ex_metadata.go │ │ │ ├── msg_video_ex_multitrack.go │ │ │ ├── msg_video_ex_sequence_end.go │ │ │ ├── msg_video_ex_sequence_start.go │ │ │ ├── opus_id_header.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── readwriter.go │ │ │ ├── readwriter_test.go │ │ │ ├── testdata │ │ │ │ └── fuzz │ │ │ │ │ └── FuzzReader │ │ │ │ │ ├── 05172fb3869a3972 │ │ │ │ │ ├── 05d2521061b772dd │ │ │ │ │ ├── 05d9ba0f9aad1a7f │ │ │ │ │ ├── 06f5bdb4e0ba6885 │ │ │ │ │ ├── 105635bd94dfe048 │ │ │ │ │ ├── 158d13fe96bff3cf │ │ │ │ │ ├── 21f6cf04358dfa71 │ │ │ │ │ ├── 23ef7989663ea23d │ │ │ │ │ ├── 2a18e30b05c133c9 │ │ │ │ │ ├── 2fb5da434799f2aa │ │ │ │ │ ├── 31a26997abe34698 │ │ │ │ │ ├── 35202254b97bc326 │ │ │ │ │ ├── 373e7702328587a8 │ │ │ │ │ ├── 378ec2a143cd2612 │ │ │ │ │ ├── 39f38a6fb2bbbbb3 │ │ │ │ │ ├── 3bcc52bfc7a4dab7 │ │ │ │ │ ├── 420fac969d79c3d0 │ │ │ │ │ ├── 5805dd8ebdbd7064 │ │ │ │ │ ├── 64e5c42dd1ecb0c2 │ │ │ │ │ ├── 6b1d357b508b38a4 │ │ │ │ │ ├── 6f2dcd363bea1d98 │ │ │ │ │ ├── 6f557bbcba30c3e8 │ │ │ │ │ ├── 857f71dbea87237e │ │ │ │ │ ├── 898db8bfed216016 │ │ │ │ │ ├── 8a81d89bb8b52d87 │ │ │ │ │ ├── 904ada19512a3bc4 │ │ │ │ │ ├── 920c0b072d6284a2 │ │ │ │ │ ├── 96fcc4dc967cb84a │ │ │ │ │ ├── 9a651b61e58fe16f │ │ │ │ │ ├── 9f06d9ce342f9e6e │ │ │ │ │ ├── a016402385fe2568 │ │ │ │ │ ├── a392a07f6f19c310 │ │ │ │ │ ├── b18a01392c7c91e9 │ │ │ │ │ ├── b79b306523beed92 │ │ │ │ │ ├── b89c469e58812fbc │ │ │ │ │ ├── c365544cf81d8e83 │ │ │ │ │ ├── c36baa3576990ca1 │ │ │ │ │ ├── c5d82dafaa66f3b7 │ │ │ │ │ ├── d4429f5abb9984ec │ │ │ │ │ ├── e03eb38bb86be03c │ │ │ │ │ ├── e4faf5e7df68d2f1 │ │ │ │ │ ├── ec85fcf226dbe44e │ │ │ │ │ ├── f15437eae190851e │ │ │ │ │ └── f244ff2f55d1103f │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── rawmessage │ │ │ ├── message.go │ │ │ ├── reader.go │ │ │ ├── reader_test.go │ │ │ ├── testdata │ │ │ │ └── fuzz │ │ │ │ │ └── FuzzReader │ │ │ │ │ ├── 19981bffc2abbaf1 │ │ │ │ │ ├── 2a3abe67115a80dc │ │ │ │ │ ├── 321edca93ba341df │ │ │ │ │ └── 7f07c167964a9467 │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── rc4_readwriter.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── server_conn.go │ │ ├── server_conn_test.go │ │ ├── to_stream.go │ │ ├── to_stream_test.go │ │ ├── writer.go │ │ └── writer_test.go │ ├── rtsp │ │ ├── credentials.go │ │ ├── credentials_test.go │ │ └── to_stream.go │ ├── tls │ │ └── tls_config.go │ ├── webrtc │ │ ├── from_stream.go │ │ ├── from_stream_test.go │ │ ├── incoming_track.go │ │ ├── outgoing_track.go │ │ ├── peer_connection.go │ │ ├── peer_connection_test.go │ │ ├── to_stream.go │ │ └── to_stream_test.go │ ├── websocket │ │ ├── serverconn.go │ │ └── serverconn_test.go │ └── whip │ │ ├── client.go │ │ ├── client_test.go │ │ ├── ice_fragment.go │ │ ├── ice_fragment_test.go │ │ ├── link_header.go │ │ └── link_header_test.go ├── recordcleaner │ ├── cleaner.go │ └── cleaner_test.go ├── recorder │ ├── format.go │ ├── format_fmp4.go │ ├── format_fmp4_part.go │ ├── format_fmp4_segment.go │ ├── format_fmp4_track.go │ ├── format_mpegts.go │ ├── format_mpegts_segment.go │ ├── recorder.go │ ├── recorder_instance.go │ └── recorder_test.go ├── recordstore │ ├── path.go │ ├── path_test.go │ ├── recordstore.go │ ├── segment.go │ └── segment_test.go ├── restrictnetwork │ └── restrict_network.go ├── rlimit │ ├── rlimit_unix.go │ └── rlimit_win.go ├── servers │ ├── hls │ │ ├── hlsjsdownloader │ │ │ ├── HASH │ │ │ ├── VERSION │ │ │ └── main.go │ │ ├── http_server.go │ │ ├── index.html │ │ ├── muxer.go │ │ ├── muxer_instance.go │ │ ├── server.go │ │ └── server_test.go │ ├── rtmp │ │ ├── conn.go │ │ ├── listener.go │ │ ├── server.go │ │ └── server_test.go │ ├── rtsp │ │ ├── conn.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── session.go │ ├── srt │ │ ├── conn.go │ │ ├── listener.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── streamid.go │ │ └── streamid_test.go │ └── webrtc │ │ ├── http_server.go │ │ ├── publish_index.html │ │ ├── publisher.js │ │ ├── read_index.html │ │ ├── reader.js │ │ ├── server.go │ │ ├── server_test.go │ │ └── session.go ├── staticsources │ ├── handler.go │ ├── hls │ │ ├── source.go │ │ └── source_test.go │ ├── rpicamera │ │ ├── camera.go │ │ ├── camera_32.go │ │ ├── camera_64.go │ │ ├── camera_disabled.go │ │ ├── camera_download.go │ │ ├── mtxrpicamdownloader │ │ │ ├── HASH_MTXRPICAM_32_TAR_GZ │ │ │ ├── HASH_MTXRPICAM_64_TAR_GZ │ │ │ ├── VERSION │ │ │ └── main.go │ │ ├── params.go │ │ ├── params_serialize.go │ │ ├── pipe.go │ │ └── source.go │ ├── rtmp │ │ ├── source.go │ │ └── source_test.go │ ├── rtsp │ │ ├── source.go │ │ └── source_test.go │ ├── srt │ │ ├── source.go │ │ └── source_test.go │ ├── udp │ │ ├── source.go │ │ └── source_test.go │ └── webrtc │ │ ├── source.go │ │ └── source_test.go ├── stream │ ├── stream.go │ ├── stream_format.go │ ├── stream_media.go │ └── stream_reader.go ├── test │ ├── auth_manager.go │ ├── formats.go │ ├── logger.go │ ├── medias.go │ ├── path_manager.go │ ├── source_tester.go │ ├── temp_file.go │ └── tls_cert.go ├── testapidocs │ └── apidocs_test.go ├── teste2e │ ├── build_images_test.go │ ├── hls_manager_test.go │ ├── images │ │ ├── ffmpeg │ │ │ ├── Dockerfile │ │ │ ├── emptyvideo.mkv │ │ │ ├── emptyvideoaudio.mkv │ │ │ └── start.sh │ │ ├── gstreamer │ │ │ ├── Dockerfile │ │ │ ├── emptyvideo.mkv │ │ │ ├── exitafterframe.c │ │ │ └── start.sh │ │ └── vlc │ │ │ ├── Dockerfile │ │ │ └── start.sh │ ├── rtsp_server_test.go │ └── tests_test.go └── unit │ ├── ac3.go │ ├── av1.go │ ├── base.go │ ├── g711.go │ ├── generic.go │ ├── h264.go │ ├── h265.go │ ├── lpcm.go │ ├── mjpeg.go │ ├── mpeg1_audio.go │ ├── mpeg1_video.go │ ├── mpeg4_audio.go │ ├── mpeg4_video.go │ ├── opus.go │ ├── unit.go │ ├── vp8.go │ └── vp9.go ├── logo.png ├── main.go ├── mediamtx.yml └── scripts ├── apidocs.mk ├── binaries.mk ├── dockerhub.mk ├── format.mk ├── lint.mk ├── mod-tidy.mk ├── run.mk ├── test-e2e.mk └── test.mk /.dockerignore: -------------------------------------------------------------------------------- 1 | # do not add .git, since it is needed to extract the tag 2 | # do not add /binaries, since it is needed by Docker images 3 | /tmp 4 | /coverage*.txt 5 | /apidocs/*.html 6 | /internal/core/VERSION 7 | /internal/servers/hls/hls.min.js 8 | /internal/staticsources/rpicamera/mtxrpicam_*/ 9 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/questions.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: markdown 3 | attributes: 4 | value: | 5 | Please create a discussion FOR EACH question. Do not ask for multiple questions all at once, otherwise they'll probably never get all answered. 6 | 7 | - type: textarea 8 | attributes: 9 | label: Question 10 | validations: 11 | required: true 12 | 13 | - type: markdown 14 | attributes: 15 | value: | 16 | Note: If you are asking for help because you're having trouble doing something, provide enough informations to replicate the problem. In particular, include in the question: 17 | 18 | * the server version you are using 19 | * precise instructions on how to replicate the problem 20 | * server logs with setting `logLevel` set to `debug` 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Question 5 | url: https://github.com/bluenviron/mediamtx/discussions/new?category=questions 6 | about: Ask the community for help 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Share ideas for new features 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please create a request FOR EACH feature. Do not report multiple features in a single request, otherwise they'll probably never get all implemented. 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the feature 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/code_lint.yml: -------------------------------------------------------------------------------- 1 | name: code_lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | golangci_lint: 11 | runs-on: ubuntu-22.04 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version: "1.24" 21 | 22 | - run: go generate ./... 23 | 24 | - uses: golangci/golangci-lint-action@v8 25 | with: 26 | version: v2.1.6 27 | 28 | mod_tidy: 29 | runs-on: ubuntu-22.04 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: actions/setup-go@v3 35 | with: 36 | go-version: "1.24" 37 | 38 | - run: make lint-mod-tidy 39 | 40 | api_docs: 41 | runs-on: ubuntu-22.04 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - run: make lint-apidocs 47 | -------------------------------------------------------------------------------- /.github/workflows/code_test.yml: -------------------------------------------------------------------------------- 1 | name: code_test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test_64: 11 | runs-on: ubuntu-22.04 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - run: make test 19 | 20 | - uses: codecov/codecov-action@v3 21 | with: 22 | token: ${{ secrets.CODECOV_TOKEN }} 23 | 24 | test_32: 25 | runs-on: ubuntu-22.04 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - run: make test-32 33 | 34 | test_e2e: 35 | runs-on: ubuntu-22.04 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - uses: actions/setup-go@v3 43 | with: 44 | go-version: "1.24" 45 | 46 | - run: make test-e2e-nodocker 47 | -------------------------------------------------------------------------------- /.github/workflows/issue_lock.yml: -------------------------------------------------------------------------------- 1 | name: issue_lock 2 | 3 | on: 4 | schedule: 5 | - cron: '40 15 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | issue_lock: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - uses: actions/github-script@v6 14 | with: 15 | github-token: ${{ secrets.GITHUB_TOKEN }} 16 | script: | 17 | const { repo: { owner, repo } } = context; 18 | 19 | const now = new Date(); 20 | 21 | for await (const res of github.paginate.iterator( 22 | github.rest.issues.listForRepo, { 23 | owner, 24 | repo, 25 | state: 'closed', 26 | })) { 27 | for (const issue of res.data) { 28 | if (issue.locked) { 29 | continue; 30 | } 31 | 32 | if ((now - new Date(issue.closed_at)) < 1000*60*60*24*31*6) { 33 | continue; 34 | } 35 | 36 | if (!issue.pull_request) { 37 | await github.rest.issues.createComment({ 38 | owner, 39 | repo, 40 | issue_number: issue.number, 41 | body: 'This issue is being locked automatically because it has been closed for more than 6 months.\n' 42 | + 'Please open a new issue in case you encounter a similar problem.', 43 | }); 44 | } 45 | 46 | github.rest.issues.lock({ 47 | owner, 48 | repo, 49 | issue_number: issue.number, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/nightly_binaries.yml: -------------------------------------------------------------------------------- 1 | name: nightly_binaries 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | nightly_binaries: 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - run: make binaries 16 | env: 17 | CHECKSUM: '1' 18 | 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: binaries 22 | path: binaries 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /binaries 3 | /coverage*.txt 4 | /apidocs/*.html 5 | /internal/core/VERSION 6 | /internal/servers/hls/hls.min.js 7 | /internal/staticsources/rpicamera/mtxrpicam_*/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 aler9 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BASE_IMAGE = golang:1.24-alpine3.20 2 | LINT_IMAGE = golangci/golangci-lint:v2.1.6 3 | NODE_IMAGE = node:20-alpine3.20 4 | 5 | .PHONY: $(shell ls) 6 | 7 | help: 8 | @echo "usage: make [action]" 9 | @echo "" 10 | @echo "available actions:" 11 | @echo "" 12 | @echo " mod-tidy run go mod tidy" 13 | @echo " format format source files" 14 | @echo " test run tests" 15 | @echo " test-32 run tests on a 32-bit system" 16 | @echo " test-e2e run end-to-end tests" 17 | @echo " lint run linters" 18 | @echo " run run app" 19 | @echo " apidocs generate api docs HTML" 20 | @echo " binaries build binaries for all platforms" 21 | @echo " dockerhub build and push images to Docker Hub" 22 | @echo "" 23 | 24 | blank := 25 | define NL 26 | 27 | $(blank) 28 | endef 29 | 30 | include scripts/*.mk 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Vulnerabilities can be reported privately by using the [Security Advisory](https://github.com/bluenviron/mediamtx/security/advisories/new) feature of GitHub. 4 | -------------------------------------------------------------------------------- /docker/ffmpeg-rpi.Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | FROM --platform=linux/amd64 scratch AS binaries 3 | 4 | ADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6 5 | ADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7 6 | ADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64 7 | 8 | ################################################################# 9 | 10 | FROM --platform=linux/arm/v6 balenalib/raspberry-pi:bullseye-run-20240508 AS base-arm-v6 11 | FROM --platform=linux/arm/v7 balenalib/raspberry-pi:bullseye-run-20240508 AS base-arm-v7 12 | FROM --platform=linux/arm64 balenalib/raspberrypi3-64:bullseye-run-20240429 AS base-arm64 13 | 14 | ################################################################# 15 | FROM --platform=linux/amd64 scratch AS base 16 | 17 | COPY --from=base-arm-v6 / /linux/arm/v6 18 | COPY --from=base-arm-v7 / /linux/arm/v7 19 | COPY --from=base-arm64 / /linux/arm64 20 | 21 | ################################################################# 22 | FROM scratch 23 | 24 | ARG TARGETPLATFORM 25 | COPY --from=base /$TARGETPLATFORM / 26 | 27 | RUN apt update \ 28 | && apt install -y --no-install-recommends ffmpeg \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | COPY --from=binaries /$TARGETPLATFORM / 32 | 33 | ENTRYPOINT [ "/mediamtx" ] 34 | -------------------------------------------------------------------------------- /docker/ffmpeg.Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | FROM --platform=linux/amd64 scratch AS binaries 3 | 4 | ADD binaries/mediamtx_*_linux_amd64.tar.gz /linux/amd64 5 | ADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6 6 | ADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7 7 | ADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64 8 | 9 | ################################################################# 10 | FROM alpine:3.20 11 | 12 | RUN apk add --no-cache ffmpeg 13 | 14 | ARG TARGETPLATFORM 15 | COPY --from=binaries /$TARGETPLATFORM / 16 | 17 | ENTRYPOINT [ "/mediamtx" ] 18 | -------------------------------------------------------------------------------- /docker/rpi.Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | FROM --platform=linux/amd64 scratch AS binaries 3 | 4 | ADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6 5 | ADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7 6 | ADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64 7 | 8 | ################################################################# 9 | 10 | FROM --platform=linux/arm/v6 balenalib/raspberry-pi:bullseye-run-20240508 AS base-arm-v6 11 | FROM --platform=linux/arm/v7 balenalib/raspberry-pi:bullseye-run-20240508 AS base-arm-v7 12 | FROM --platform=linux/arm64 balenalib/raspberrypi3-64:bullseye-run-20240429 AS base-arm64 13 | 14 | ################################################################# 15 | FROM --platform=linux/amd64 scratch AS base 16 | 17 | COPY --from=base-arm-v6 / /linux/arm/v6 18 | COPY --from=base-arm-v7 / /linux/arm/v7 19 | COPY --from=base-arm64 / /linux/arm64 20 | 21 | ################################################################# 22 | FROM scratch 23 | 24 | ARG TARGETPLATFORM 25 | COPY --from=base /$TARGETPLATFORM / 26 | 27 | COPY --from=binaries /$TARGETPLATFORM / 28 | 29 | ENTRYPOINT [ "/mediamtx" ] 30 | -------------------------------------------------------------------------------- /docker/standard.Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | FROM --platform=linux/amd64 scratch AS binaries 3 | 4 | ADD binaries/mediamtx_*_linux_amd64.tar.gz /linux/amd64 5 | ADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6 6 | ADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7 7 | ADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64 8 | 9 | ################################################################# 10 | FROM scratch 11 | 12 | ARG TARGETPLATFORM 13 | COPY --from=binaries /$TARGETPLATFORM / 14 | 15 | ENTRYPOINT [ "/mediamtx" ] 16 | -------------------------------------------------------------------------------- /internal/api/paginate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int { 10 | ritems := reflect.ValueOf(itemsPtr).Elem() 11 | 12 | itemsLen := ritems.Len() 13 | if itemsLen == 0 { 14 | return 0 15 | } 16 | 17 | pageCount := (itemsLen / itemsPerPage) 18 | if (itemsLen % itemsPerPage) != 0 { 19 | pageCount++ 20 | } 21 | 22 | minVal := page * itemsPerPage 23 | if minVal > itemsLen { 24 | minVal = itemsLen 25 | } 26 | 27 | maxVal := (page + 1) * itemsPerPage 28 | if maxVal > itemsLen { 29 | maxVal = itemsLen 30 | } 31 | 32 | ritems.Set(ritems.Slice(minVal, maxVal)) 33 | 34 | return pageCount 35 | } 36 | 37 | func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) { 38 | itemsPerPage := 100 39 | 40 | if itemsPerPageStr != "" { 41 | tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31) 42 | if err != nil { 43 | return 0, err 44 | } 45 | itemsPerPage = int(tmp) 46 | 47 | if itemsPerPage == 0 { 48 | return 0, fmt.Errorf("invalid items per page") 49 | } 50 | } 51 | 52 | page := 0 53 | 54 | if pageStr != "" { 55 | tmp, err := strconv.ParseUint(pageStr, 10, 31) 56 | if err != nil { 57 | return 0, err 58 | } 59 | page = int(tmp) 60 | } 61 | 62 | return paginate2(itemsPtr, itemsPerPage, page), nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/api/paginate_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPaginate(t *testing.T) { 10 | func() { 11 | items := make([]int, 5) 12 | for i := 0; i < 5; i++ { 13 | items[i] = i 14 | } 15 | 16 | pageCount, err := paginate(&items, "1", "1") 17 | require.NoError(t, err) 18 | require.Equal(t, 5, pageCount) 19 | require.Equal(t, []int{1}, items) 20 | }() 21 | 22 | func() { 23 | items := make([]int, 5) 24 | for i := 0; i < 5; i++ { 25 | items[i] = i 26 | } 27 | 28 | pageCount, err := paginate(&items, "3", "2") 29 | require.NoError(t, err) 30 | require.Equal(t, 2, pageCount) 31 | require.Equal(t, []int{}, items) 32 | }() 33 | 34 | func() { 35 | items := make([]int, 6) 36 | for i := 0; i < 6; i++ { 37 | items[i] = i 38 | } 39 | 40 | pageCount, err := paginate(&items, "4", "1") 41 | require.NoError(t, err) 42 | require.Equal(t, 2, pageCount) 43 | require.Equal(t, []int{4, 5}, items) 44 | }() 45 | 46 | func() { 47 | items := make([]int, 0) 48 | 49 | pageCount, err := paginate(&items, "1", "0") 50 | require.NoError(t, err) 51 | require.Equal(t, 0, pageCount) 52 | require.Equal(t, []int{}, items) 53 | }() 54 | } 55 | 56 | func FuzzPaginate(f *testing.F) { 57 | f.Fuzz(func(_ *testing.T, str1 string, str2 string) { 58 | items := make([]int, 6) 59 | for i := 0; i < 6; i++ { 60 | items[i] = i 61 | } 62 | 63 | paginate(&items, str1, str2) //nolint:errcheck 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("A") 3 | string("0") 4 | -------------------------------------------------------------------------------- /internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("1") 3 | string("A") 4 | -------------------------------------------------------------------------------- /internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("0") 3 | string("") 4 | -------------------------------------------------------------------------------- /internal/auth/credentials.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Credentials is a set of credentials (either user+pass or a token). 4 | type Credentials struct { 5 | User string 6 | Pass string 7 | Token string 8 | } 9 | -------------------------------------------------------------------------------- /internal/auth/jwt_claims.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf" 8 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 9 | "github.com/golang-jwt/jwt/v5" 10 | ) 11 | 12 | type jwtClaims struct { 13 | jwt.RegisteredClaims 14 | permissionsKey string 15 | permissions []conf.AuthInternalUserPermission 16 | } 17 | 18 | func (c *jwtClaims) UnmarshalJSON(b []byte) error { 19 | err := json.Unmarshal(b, &c.RegisteredClaims) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | var claimMap map[string]json.RawMessage 25 | err = json.Unmarshal(b, &claimMap) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | rawPermissions, ok := claimMap[c.permissionsKey] 31 | if !ok { 32 | return fmt.Errorf("claim '%s' not found inside JWT", c.permissionsKey) 33 | } 34 | 35 | err = jsonwrapper.Unmarshal(rawPermissions, &c.permissions) 36 | if err != nil { 37 | var str string 38 | err = json.Unmarshal(rawPermissions, &str) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = jsonwrapper.Unmarshal([]byte(str), &c.permissions) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/auth/request.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/bluenviron/mediamtx/internal/conf" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Protocol is a protocol. 11 | type Protocol string 12 | 13 | // protocols. 14 | const ( 15 | ProtocolRTSP Protocol = "rtsp" 16 | ProtocolRTMP Protocol = "rtmp" 17 | ProtocolHLS Protocol = "hls" 18 | ProtocolWebRTC Protocol = "webrtc" 19 | ProtocolSRT Protocol = "srt" 20 | ) 21 | 22 | // Request is an authentication request. 23 | type Request struct { 24 | Action conf.AuthAction 25 | Path string // only for ActionPublish, ActionRead, ActionPlayback 26 | Query string 27 | Protocol Protocol // only for ActionPublish, ActionRead 28 | ID *uuid.UUID // only for ActionPublish, ActionRead 29 | Credentials *Credentials 30 | IP net.IP 31 | CustomVerifyFunc func(expectedUser string, expectedPass string) bool 32 | } 33 | -------------------------------------------------------------------------------- /internal/certloader/certloader_test.go: -------------------------------------------------------------------------------- 1 | package certloader 2 | 3 | import ( 4 | "crypto/tls" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/bluenviron/mediamtx/internal/test" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestCertReload(t *testing.T) { 14 | testData, err := tls.X509KeyPair(test.TLSCertPub, test.TLSCertKey) 15 | require.NoError(t, err) 16 | 17 | serverCertPath, err := test.CreateTempFile(test.TLSCertPub) 18 | require.NoError(t, err) 19 | defer os.Remove(serverCertPath) 20 | 21 | serverKeyPath, err := test.CreateTempFile(test.TLSCertKey) 22 | require.NoError(t, err) 23 | defer os.Remove(serverKeyPath) 24 | 25 | loader := &CertLoader{ 26 | CertPath: serverCertPath, 27 | KeyPath: serverKeyPath, 28 | Parent: test.NilLogger, 29 | } 30 | err = loader.Initialize() 31 | require.NoError(t, err) 32 | defer loader.Close() 33 | 34 | getCert := loader.GetCertificate() 35 | require.NotNil(t, getCert) 36 | 37 | cert, err := getCert(nil) 38 | require.NoError(t, err) 39 | require.NotNil(t, cert) 40 | require.Equal(t, &testData, cert) 41 | 42 | testData, err = tls.X509KeyPair(test.TLSCertPubAlt, test.TLSCertKeyAlt) 43 | require.NoError(t, err) 44 | 45 | err = os.WriteFile(serverCertPath, test.TLSCertPubAlt, 0o644) 46 | require.NoError(t, err) 47 | 48 | err = os.WriteFile(serverKeyPath, test.TLSCertKeyAlt, 0o644) 49 | require.NoError(t, err) 50 | 51 | time.Sleep(1 * time.Second) 52 | 53 | cert, err = getCert(nil) 54 | require.NoError(t, err) 55 | require.NotNil(t, cert) 56 | require.Equal(t, &testData, cert) 57 | } 58 | -------------------------------------------------------------------------------- /internal/conf/auth_action.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | ) 9 | 10 | // AuthAction is an authentication action. 11 | type AuthAction string 12 | 13 | // auth actions 14 | const ( 15 | AuthActionPublish AuthAction = "publish" 16 | AuthActionRead AuthAction = "read" 17 | AuthActionPlayback AuthAction = "playback" 18 | AuthActionAPI AuthAction = "api" 19 | AuthActionMetrics AuthAction = "metrics" 20 | AuthActionPprof AuthAction = "pprof" 21 | ) 22 | 23 | // MarshalJSON implements json.Marshaler. 24 | func (d AuthAction) MarshalJSON() ([]byte, error) { 25 | return json.Marshal(string(d)) 26 | } 27 | 28 | // UnmarshalJSON implements json.Unmarshaler. 29 | func (d *AuthAction) UnmarshalJSON(b []byte) error { 30 | var in string 31 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 32 | return err 33 | } 34 | 35 | switch in { 36 | case string(AuthActionPublish), 37 | string(AuthActionRead), 38 | string(AuthActionPlayback), 39 | string(AuthActionAPI), 40 | string(AuthActionMetrics), 41 | string(AuthActionPprof): 42 | *d = AuthAction(in) 43 | 44 | default: 45 | return fmt.Errorf("invalid auth action: '%s'", in) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // UnmarshalEnv implements env.Unmarshaler. 52 | func (d *AuthAction) UnmarshalEnv(_ string, v string) error { 53 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 54 | } 55 | -------------------------------------------------------------------------------- /internal/conf/auth_method.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | ) 9 | 10 | // AuthMethod is an authentication method. 11 | type AuthMethod int 12 | 13 | // authentication methods. 14 | const ( 15 | AuthMethodInternal AuthMethod = iota 16 | AuthMethodHTTP 17 | AuthMethodJWT 18 | ) 19 | 20 | // MarshalJSON implements json.Marshaler. 21 | func (d AuthMethod) MarshalJSON() ([]byte, error) { 22 | var out string 23 | 24 | switch d { 25 | case AuthMethodInternal: 26 | out = "internal" 27 | 28 | case AuthMethodHTTP: 29 | out = "http" 30 | 31 | default: 32 | out = "jwt" 33 | } 34 | 35 | return json.Marshal(out) 36 | } 37 | 38 | // UnmarshalJSON implements json.Unmarshaler. 39 | func (d *AuthMethod) UnmarshalJSON(b []byte) error { 40 | var in string 41 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 42 | return err 43 | } 44 | 45 | switch in { 46 | case "internal": 47 | *d = AuthMethodInternal 48 | 49 | case "http": 50 | *d = AuthMethodHTTP 51 | 52 | case "jwt": 53 | *d = AuthMethodJWT 54 | 55 | default: 56 | return fmt.Errorf("invalid authMethod: '%s'", in) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // UnmarshalEnv implements env.Unmarshaler. 63 | func (d *AuthMethod) UnmarshalEnv(_ string, v string) error { 64 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 65 | } 66 | -------------------------------------------------------------------------------- /internal/conf/decrypt/decrypt.go: -------------------------------------------------------------------------------- 1 | // Package decrypt contains the Decrypt function. 2 | package decrypt 3 | 4 | import ( 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "golang.org/x/crypto/nacl/secretbox" 9 | ) 10 | 11 | // Decrypt decrypts the configuration with the given key. 12 | func Decrypt(key string, byts []byte) ([]byte, error) { 13 | enc, err := base64.StdEncoding.DecodeString(string(byts)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | var secretKey [32]byte 19 | copy(secretKey[:], key) 20 | 21 | var decryptNonce [24]byte 22 | copy(decryptNonce[:], enc[:24]) 23 | decrypted, ok := secretbox.Open(nil, enc[24:], &decryptNonce, &secretKey) 24 | if !ok { 25 | return nil, fmt.Errorf("decryption error") 26 | } 27 | 28 | return decrypted, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/conf/duration_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var casesDuration = []struct { 11 | name string 12 | dec Duration 13 | enc string 14 | }{ 15 | { 16 | "standard", 17 | Duration(13456 * time.Second), 18 | `"3h44m16s"`, 19 | }, 20 | { 21 | "days", 22 | Duration(50 * 13456 * time.Second), 23 | `"7d18h53m20s"`, 24 | }, 25 | { 26 | "days negative", 27 | Duration(-50 * 13456 * time.Second), 28 | `"-7d18h53m20s"`, 29 | }, 30 | { 31 | "days even", 32 | Duration(7 * 24 * time.Hour), 33 | `"7d"`, 34 | }, 35 | } 36 | 37 | func TestDurationUnmarshal(t *testing.T) { 38 | for _, ca := range casesDuration { 39 | t.Run(ca.name, func(t *testing.T) { 40 | var dec Duration 41 | err := dec.UnmarshalJSON([]byte(ca.enc)) 42 | require.NoError(t, err) 43 | require.Equal(t, ca.dec, dec) 44 | }) 45 | } 46 | } 47 | 48 | func TestDurationMarshal(t *testing.T) { 49 | for _, ca := range casesDuration { 50 | t.Run(ca.name, func(t *testing.T) { 51 | enc, err := ca.dec.MarshalJSON() 52 | require.NoError(t, err) 53 | require.Equal(t, ca.enc, string(enc)) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/conf/encryption.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | ) 9 | 10 | // Encryption is the rtspEncryption / rtmpEncryption parameter. 11 | type Encryption int 12 | 13 | // values. 14 | const ( 15 | EncryptionNo Encryption = iota 16 | EncryptionOptional 17 | EncryptionStrict 18 | ) 19 | 20 | // MarshalJSON implements json.Marshaler. 21 | func (d Encryption) MarshalJSON() ([]byte, error) { 22 | var out string 23 | 24 | switch d { 25 | case EncryptionNo: 26 | out = "no" 27 | 28 | case EncryptionOptional: 29 | out = "optional" 30 | 31 | default: 32 | out = "strict" 33 | } 34 | 35 | return json.Marshal(out) 36 | } 37 | 38 | // UnmarshalJSON implements json.Unmarshaler. 39 | func (d *Encryption) UnmarshalJSON(b []byte) error { 40 | var in string 41 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 42 | return err 43 | } 44 | 45 | switch in { 46 | case "no", "false": 47 | *d = EncryptionNo 48 | 49 | case "optional": 50 | *d = EncryptionOptional 51 | 52 | case "strict", "yes", "true": 53 | *d = EncryptionStrict 54 | 55 | default: 56 | return fmt.Errorf("invalid encryption: '%s'", in) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // UnmarshalEnv implements env.Unmarshaler. 63 | func (d *Encryption) UnmarshalEnv(_ string, v string) error { 64 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 65 | } 66 | -------------------------------------------------------------------------------- /internal/conf/global.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | ) 7 | 8 | var globalValuesType = func() reflect.Type { 9 | var fields []reflect.StructField 10 | rt := reflect.TypeOf(Conf{}) 11 | nf := rt.NumField() 12 | 13 | for i := 0; i < nf; i++ { 14 | f := rt.Field(i) 15 | j := f.Tag.Get("json") 16 | 17 | if j != "-" && j != "pathDefaults" && j != "paths" { 18 | fields = append(fields, reflect.StructField{ 19 | Name: f.Name, 20 | Type: f.Type, 21 | Tag: f.Tag, 22 | }) 23 | } 24 | } 25 | 26 | return reflect.StructOf(fields) 27 | }() 28 | 29 | func newGlobalValues() interface{} { 30 | return reflect.New(globalValuesType).Interface() 31 | } 32 | 33 | // Global is the global part of Conf. 34 | type Global struct { 35 | Values interface{} 36 | } 37 | 38 | // MarshalJSON implements json.Marshaler. 39 | func (p *Global) MarshalJSON() ([]byte, error) { 40 | return json.Marshal(p.Values) 41 | } 42 | -------------------------------------------------------------------------------- /internal/conf/hls_variant.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/gohlslib/v2" 8 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 9 | ) 10 | 11 | // HLSVariant is the hlsVariant parameter. 12 | type HLSVariant gohlslib.MuxerVariant 13 | 14 | // MarshalJSON implements json.Marshaler. 15 | func (d HLSVariant) MarshalJSON() ([]byte, error) { 16 | var out string 17 | 18 | switch d { 19 | case HLSVariant(gohlslib.MuxerVariantMPEGTS): 20 | out = "mpegts" 21 | 22 | case HLSVariant(gohlslib.MuxerVariantFMP4): 23 | out = "fmp4" 24 | 25 | default: 26 | out = "lowLatency" 27 | } 28 | 29 | return json.Marshal(out) 30 | } 31 | 32 | // UnmarshalJSON implements json.Unmarshaler. 33 | func (d *HLSVariant) UnmarshalJSON(b []byte) error { 34 | var in string 35 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 36 | return err 37 | } 38 | 39 | switch in { 40 | case "mpegts": 41 | *d = HLSVariant(gohlslib.MuxerVariantMPEGTS) 42 | 43 | case "fmp4": 44 | *d = HLSVariant(gohlslib.MuxerVariantFMP4) 45 | 46 | case "lowLatency": 47 | *d = HLSVariant(gohlslib.MuxerVariantLowLatency) 48 | 49 | default: 50 | return fmt.Errorf("invalid HLS variant: '%s'", in) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // UnmarshalEnv implements env.Unmarshaler. 57 | func (d *HLSVariant) UnmarshalEnv(_ string, v string) error { 58 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 59 | } 60 | -------------------------------------------------------------------------------- /internal/conf/jsonwrapper/unmarshal.go: -------------------------------------------------------------------------------- 1 | // Package jsonwrapper contains a JSON unmarshaler. 2 | package jsonwrapper 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "io" 8 | ) 9 | 10 | // Unmarshal decodes JSON. 11 | // It returns an error if a non-existing field is found. 12 | func Unmarshal(buf []byte, dest any) error { 13 | return Decode(bytes.NewReader(buf), dest) 14 | } 15 | 16 | // Decode decodes JSON. 17 | // It returns an error if a non-existing field is found. 18 | func Decode(r io.Reader, dest any) error { 19 | d := json.NewDecoder(r) 20 | d.DisallowUnknownFields() 21 | return d.Decode(dest) 22 | } 23 | -------------------------------------------------------------------------------- /internal/conf/log_level.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | "github.com/bluenviron/mediamtx/internal/logger" 9 | ) 10 | 11 | // LogLevel is the logLevel parameter. 12 | type LogLevel logger.Level 13 | 14 | // MarshalJSON implements json.Marshaler. 15 | func (d LogLevel) MarshalJSON() ([]byte, error) { 16 | var out string 17 | 18 | switch d { 19 | case LogLevel(logger.Error): 20 | out = "error" 21 | 22 | case LogLevel(logger.Warn): 23 | out = "warn" 24 | 25 | case LogLevel(logger.Info): 26 | out = "info" 27 | 28 | default: 29 | out = "debug" 30 | } 31 | 32 | return json.Marshal(out) 33 | } 34 | 35 | // UnmarshalJSON implements json.Unmarshaler. 36 | func (d *LogLevel) UnmarshalJSON(b []byte) error { 37 | var in string 38 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 39 | return err 40 | } 41 | 42 | switch in { 43 | case "error": 44 | *d = LogLevel(logger.Error) 45 | 46 | case "warn": 47 | *d = LogLevel(logger.Warn) 48 | 49 | case "info": 50 | *d = LogLevel(logger.Info) 51 | 52 | case "debug": 53 | *d = LogLevel(logger.Debug) 54 | 55 | default: 56 | return fmt.Errorf("invalid log level: '%s'", in) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // UnmarshalEnv implements env.Unmarshaler. 63 | func (d *LogLevel) UnmarshalEnv(_ string, v string) error { 64 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 65 | } 66 | -------------------------------------------------------------------------------- /internal/conf/optional_global.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 9 | ) 10 | 11 | var optionalGlobalValuesType = func() reflect.Type { 12 | var fields []reflect.StructField 13 | rt := reflect.TypeOf(Conf{}) 14 | nf := rt.NumField() 15 | 16 | for i := 0; i < nf; i++ { 17 | f := rt.Field(i) 18 | j := f.Tag.Get("json") 19 | 20 | if j != "-" && j != "pathDefaults" && j != "paths" { 21 | if !strings.Contains(j, ",omitempty") { 22 | j += ",omitempty" 23 | } 24 | 25 | typ := f.Type 26 | if typ.Kind() != reflect.Pointer { 27 | typ = reflect.PointerTo(typ) 28 | } 29 | 30 | fields = append(fields, reflect.StructField{ 31 | Name: f.Name, 32 | Type: typ, 33 | Tag: reflect.StructTag(`json:"` + j + `"`), 34 | }) 35 | } 36 | } 37 | 38 | return reflect.StructOf(fields) 39 | }() 40 | 41 | func newOptionalGlobalValues() interface{} { 42 | return reflect.New(optionalGlobalValuesType).Interface() 43 | } 44 | 45 | // OptionalGlobal is a Conf whose values can all be optional. 46 | type OptionalGlobal struct { 47 | Values interface{} 48 | } 49 | 50 | // UnmarshalJSON implements json.Unmarshaler. 51 | func (p *OptionalGlobal) UnmarshalJSON(b []byte) error { 52 | p.Values = newOptionalGlobalValues() 53 | return jsonwrapper.Unmarshal(b, p.Values) 54 | } 55 | 56 | // MarshalJSON implements json.Marshaler. 57 | func (p *OptionalGlobal) MarshalJSON() ([]byte, error) { 58 | return json.Marshal(p.Values) 59 | } 60 | -------------------------------------------------------------------------------- /internal/conf/optional_path.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/bluenviron/mediamtx/internal/conf/env" 9 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 10 | ) 11 | 12 | var optionalPathValuesType = func() reflect.Type { 13 | var fields []reflect.StructField 14 | rt := reflect.TypeOf(Path{}) 15 | nf := rt.NumField() 16 | 17 | for i := 0; i < nf; i++ { 18 | f := rt.Field(i) 19 | j := f.Tag.Get("json") 20 | 21 | if j != "-" { 22 | if !strings.Contains(j, ",omitempty") { 23 | j += ",omitempty" 24 | } 25 | 26 | typ := f.Type 27 | if typ.Kind() != reflect.Pointer { 28 | typ = reflect.PointerTo(typ) 29 | } 30 | 31 | fields = append(fields, reflect.StructField{ 32 | Name: f.Name, 33 | Type: typ, 34 | Tag: reflect.StructTag(`json:"` + j + `"`), 35 | }) 36 | } 37 | } 38 | 39 | return reflect.StructOf(fields) 40 | }() 41 | 42 | func newOptionalPathValues() interface{} { 43 | return reflect.New(optionalPathValuesType).Interface() 44 | } 45 | 46 | // OptionalPath is a Path whose values can all be optional. 47 | type OptionalPath struct { 48 | Values interface{} 49 | } 50 | 51 | // UnmarshalJSON implements json.Unmarshaler. 52 | func (p *OptionalPath) UnmarshalJSON(b []byte) error { 53 | p.Values = newOptionalPathValues() 54 | return jsonwrapper.Unmarshal(b, p.Values) 55 | } 56 | 57 | // UnmarshalEnv implements env.Unmarshaler. 58 | func (p *OptionalPath) UnmarshalEnv(prefix string, _ string) error { 59 | if p.Values == nil { 60 | p.Values = newOptionalPathValues() 61 | } 62 | return env.Load(prefix, p.Values) 63 | } 64 | 65 | // MarshalJSON implements json.Marshaler. 66 | func (p *OptionalPath) MarshalJSON() ([]byte, error) { 67 | return json.Marshal(p.Values) 68 | } 69 | -------------------------------------------------------------------------------- /internal/conf/record_format.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | ) 9 | 10 | // RecordFormat is the recordFormat parameter. 11 | type RecordFormat int 12 | 13 | // supported values. 14 | const ( 15 | RecordFormatFMP4 RecordFormat = iota 16 | RecordFormatMPEGTS 17 | ) 18 | 19 | // MarshalJSON implements json.Marshaler. 20 | func (d RecordFormat) MarshalJSON() ([]byte, error) { 21 | var out string 22 | 23 | switch d { 24 | case RecordFormatMPEGTS: 25 | out = "mpegts" 26 | 27 | default: 28 | out = "fmp4" 29 | } 30 | 31 | return json.Marshal(out) 32 | } 33 | 34 | // UnmarshalJSON implements json.Unmarshaler. 35 | func (d *RecordFormat) UnmarshalJSON(b []byte) error { 36 | var in string 37 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 38 | return err 39 | } 40 | 41 | switch in { 42 | case "mpegts": 43 | *d = RecordFormatMPEGTS 44 | 45 | case "fmp4": 46 | *d = RecordFormatFMP4 47 | 48 | default: 49 | return fmt.Errorf("invalid record format '%s'", in) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // UnmarshalEnv implements env.Unmarshaler. 56 | func (d *RecordFormat) UnmarshalEnv(_ string, v string) error { 57 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 58 | } 59 | -------------------------------------------------------------------------------- /internal/conf/rtsp_auth_methods.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/bluenviron/gortsplib/v4/pkg/auth" 10 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 11 | ) 12 | 13 | // RTSPAuthMethods is the rtspAuthMethods parameter. 14 | type RTSPAuthMethods []auth.VerifyMethod 15 | 16 | // MarshalJSON implements json.Marshaler. 17 | func (d RTSPAuthMethods) MarshalJSON() ([]byte, error) { 18 | out := make([]string, len(d)) 19 | 20 | for i, v := range d { 21 | switch v { 22 | case auth.VerifyMethodBasic: 23 | out[i] = "basic" 24 | 25 | default: 26 | out[i] = "digest" 27 | } 28 | } 29 | 30 | sort.Strings(out) 31 | 32 | return json.Marshal(out) 33 | } 34 | 35 | // UnmarshalJSON implements json.Unmarshaler. 36 | func (d *RTSPAuthMethods) UnmarshalJSON(b []byte) error { 37 | var in []string 38 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 39 | return err 40 | } 41 | 42 | *d = nil 43 | 44 | for _, v := range in { 45 | switch v { 46 | case "basic": 47 | *d = append(*d, auth.VerifyMethodBasic) 48 | 49 | case "digest": 50 | *d = append(*d, auth.VerifyMethodDigestMD5) 51 | 52 | default: 53 | return fmt.Errorf("invalid authentication method: '%s'", v) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // UnmarshalEnv implements env.Unmarshaler. 61 | func (d *RTSPAuthMethods) UnmarshalEnv(_ string, v string) error { 62 | byts, _ := json.Marshal(strings.Split(v, ",")) 63 | return d.UnmarshalJSON(byts) 64 | } 65 | -------------------------------------------------------------------------------- /internal/conf/rtsp_range_type.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 8 | ) 9 | 10 | // RTSPRangeType is the type used in the Range header. 11 | type RTSPRangeType int 12 | 13 | // supported values. 14 | const ( 15 | RTSPRangeTypeUndefined RTSPRangeType = iota 16 | RTSPRangeTypeClock 17 | RTSPRangeTypeNPT 18 | RTSPRangeTypeSMPTE 19 | ) 20 | 21 | // MarshalJSON implements json.Marshaler. 22 | func (d RTSPRangeType) MarshalJSON() ([]byte, error) { 23 | var out string 24 | 25 | switch d { 26 | case RTSPRangeTypeClock: 27 | out = "clock" 28 | 29 | case RTSPRangeTypeNPT: 30 | out = "npt" 31 | 32 | case RTSPRangeTypeSMPTE: 33 | out = "smpte" 34 | 35 | default: 36 | out = "" 37 | } 38 | 39 | return json.Marshal(out) 40 | } 41 | 42 | // UnmarshalJSON implements json.Unmarshaler. 43 | func (d *RTSPRangeType) UnmarshalJSON(b []byte) error { 44 | var in string 45 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 46 | return err 47 | } 48 | 49 | switch in { 50 | case "clock": 51 | *d = RTSPRangeTypeClock 52 | 53 | case "npt": 54 | *d = RTSPRangeTypeNPT 55 | 56 | case "smpte": 57 | *d = RTSPRangeTypeSMPTE 58 | 59 | case "": 60 | *d = RTSPRangeTypeUndefined 61 | 62 | default: 63 | return fmt.Errorf("invalid rtsp range type: '%s'", in) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // UnmarshalEnv implements env.Unmarshaler. 70 | func (d *RTSPRangeType) UnmarshalEnv(_ string, v string) error { 71 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 72 | } 73 | -------------------------------------------------------------------------------- /internal/conf/rtsp_transport.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/bluenviron/gortsplib/v4" 8 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 9 | ) 10 | 11 | // RTSPTransport is the rtspTransport parameter. 12 | type RTSPTransport struct { 13 | *gortsplib.Transport 14 | } 15 | 16 | // MarshalJSON implements json.Marshaler. 17 | func (d RTSPTransport) MarshalJSON() ([]byte, error) { 18 | var out string 19 | 20 | if d.Transport == nil { 21 | out = "automatic" 22 | } else { 23 | switch *d.Transport { 24 | case gortsplib.TransportUDP: 25 | out = "udp" 26 | 27 | case gortsplib.TransportUDPMulticast: 28 | out = "multicast" 29 | 30 | default: 31 | out = "tcp" 32 | } 33 | } 34 | 35 | return json.Marshal(out) 36 | } 37 | 38 | // UnmarshalJSON implements json.Unmarshaler. 39 | func (d *RTSPTransport) UnmarshalJSON(b []byte) error { 40 | var in string 41 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 42 | return err 43 | } 44 | 45 | switch in { 46 | case "udp": 47 | v := gortsplib.TransportUDP 48 | d.Transport = &v 49 | 50 | case "multicast": 51 | v := gortsplib.TransportUDPMulticast 52 | d.Transport = &v 53 | 54 | case "tcp": 55 | v := gortsplib.TransportTCP 56 | d.Transport = &v 57 | 58 | case "automatic": 59 | d.Transport = nil 60 | 61 | default: 62 | return fmt.Errorf("invalid transport '%s'", in) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // UnmarshalEnv implements env.Unmarshaler. 69 | func (d *RTSPTransport) UnmarshalEnv(_ string, v string) error { 70 | return d.UnmarshalJSON([]byte(`"` + v + `"`)) 71 | } 72 | -------------------------------------------------------------------------------- /internal/conf/rtsp_transports.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/bluenviron/gortsplib/v4" 10 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 11 | ) 12 | 13 | // RTSPTransports is the rtspTransports parameter. 14 | type RTSPTransports map[gortsplib.Transport]struct{} 15 | 16 | // MarshalJSON implements json.Marshaler. 17 | func (d RTSPTransports) MarshalJSON() ([]byte, error) { 18 | out := make([]string, len(d)) 19 | i := 0 20 | 21 | for p := range d { 22 | var v string 23 | 24 | switch p { 25 | case gortsplib.TransportUDP: 26 | v = "udp" 27 | 28 | case gortsplib.TransportUDPMulticast: 29 | v = "multicast" 30 | 31 | default: 32 | v = "tcp" 33 | } 34 | 35 | out[i] = v 36 | i++ 37 | } 38 | 39 | sort.Strings(out) 40 | 41 | return json.Marshal(out) 42 | } 43 | 44 | // UnmarshalJSON implements json.Unmarshaler. 45 | func (d *RTSPTransports) UnmarshalJSON(b []byte) error { 46 | var in []string 47 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 48 | return err 49 | } 50 | 51 | *d = make(RTSPTransports) 52 | 53 | for _, proto := range in { 54 | switch proto { 55 | case "udp": 56 | (*d)[gortsplib.TransportUDP] = struct{}{} 57 | 58 | case "multicast": 59 | (*d)[gortsplib.TransportUDPMulticast] = struct{}{} 60 | 61 | case "tcp": 62 | (*d)[gortsplib.TransportTCP] = struct{}{} 63 | 64 | default: 65 | return fmt.Errorf("invalid transport: %s", proto) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // UnmarshalEnv implements env.Unmarshaler. 73 | func (d *RTSPTransports) UnmarshalEnv(_ string, v string) error { 74 | byts, _ := json.Marshal(strings.Split(v, ",")) 75 | return d.UnmarshalJSON(byts) 76 | } 77 | -------------------------------------------------------------------------------- /internal/conf/string_size.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "code.cloudfoundry.org/bytefmt" 5 | "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 6 | ) 7 | 8 | // StringSize is a size that is unmarshaled from a string. 9 | type StringSize uint64 10 | 11 | // MarshalJSON implements json.Marshaler. 12 | func (s StringSize) MarshalJSON() ([]byte, error) { 13 | return []byte(`"` + bytefmt.ByteSize(uint64(s)) + `"`), nil 14 | } 15 | 16 | // UnmarshalJSON implements json.Unmarshaler. 17 | func (s *StringSize) UnmarshalJSON(b []byte) error { 18 | var in string 19 | if err := jsonwrapper.Unmarshal(b, &in); err != nil { 20 | return err 21 | } 22 | 23 | v, err := bytefmt.ToBytes(in) 24 | if err != nil { 25 | return err 26 | } 27 | *s = StringSize(v) 28 | 29 | return nil 30 | } 31 | 32 | // UnmarshalEnv implements env.Unmarshaler. 33 | func (s *StringSize) UnmarshalEnv(_ string, v string) error { 34 | return s.UnmarshalJSON([]byte(`"` + v + `"`)) 35 | } 36 | -------------------------------------------------------------------------------- /internal/conf/webrtc_ice_server.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" 4 | 5 | // WebRTCICEServer is a WebRTC ICE Server. 6 | type WebRTCICEServer struct { 7 | URL string `json:"url"` 8 | Username string `json:"username"` 9 | Password string `json:"password"` 10 | ClientOnly bool `json:"clientOnly"` 11 | } 12 | 13 | // WebRTCICEServers is a list of WebRTCICEServer 14 | type WebRTCICEServers []WebRTCICEServer 15 | 16 | // UnmarshalJSON implements json.Unmarshaler. 17 | func (s *WebRTCICEServers) UnmarshalJSON(b []byte) error { 18 | // remove default value before loading new value 19 | // https://github.com/golang/go/issues/21092 20 | *s = nil 21 | return jsonwrapper.Unmarshal(b, (*[]WebRTCICEServer)(s)) 22 | } 23 | -------------------------------------------------------------------------------- /internal/conf/yamlwrapper/unmarshal.go: -------------------------------------------------------------------------------- 1 | // Package yamlwrapper contains a YAML unmarshaler. 2 | package yamlwrapper 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | func convertKeys(i interface{}) (interface{}, error) { 12 | switch x := i.(type) { 13 | case map[interface{}]interface{}: 14 | m2 := map[string]interface{}{} 15 | for k, v := range x { 16 | ks, ok := k.(string) 17 | if !ok { 18 | return nil, fmt.Errorf("integer keys are not supported (%v)", k) 19 | } 20 | 21 | var err error 22 | m2[ks], err = convertKeys(v) 23 | if err != nil { 24 | return nil, err 25 | } 26 | } 27 | return m2, nil 28 | 29 | case []interface{}: 30 | a2 := make([]interface{}, len(x)) 31 | for i, v := range x { 32 | var err error 33 | a2[i], err = convertKeys(v) 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | return a2, nil 39 | } 40 | 41 | return i, nil 42 | } 43 | 44 | // Unmarshal loads the configuration from YAML. 45 | func Unmarshal(buf []byte, dest interface{}) error { 46 | // load YAML into a generic map 47 | // from documentation: 48 | // "UnmarshalStrict is like Unmarshal except that any fields that are found in the data 49 | // that do not have corresponding struct members, or mapping keys that are duplicates, will result in an error." 50 | var temp interface{} 51 | err := yaml.UnmarshalStrict(buf, &temp) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // convert interface{} keys into string keys to avoid JSON errors 57 | temp, err = convertKeys(temp) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // convert the generic map into JSON 63 | buf, err = json.Marshal(temp) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // load JSON into destination 69 | return json.Unmarshal(buf, dest) 70 | } 71 | -------------------------------------------------------------------------------- /internal/core/source_redirect.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/defs" 5 | "github.com/bluenviron/mediamtx/internal/logger" 6 | ) 7 | 8 | // sourceRedirect is a source that redirects to another one. 9 | type sourceRedirect struct{} 10 | 11 | func (*sourceRedirect) Log(logger.Level, string, ...interface{}) { 12 | } 13 | 14 | // APISourceDescribe implements source. 15 | func (*sourceRedirect) APISourceDescribe() defs.APIPathSourceOrReader { 16 | return defs.APIPathSourceOrReader{ 17 | Type: "redirect", 18 | ID: "", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/core/test_on_demand/main.go: -------------------------------------------------------------------------------- 1 | // This is used for testing purposes. 2 | package main 3 | 4 | import ( 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/bluenviron/gortsplib/v4" 10 | "github.com/bluenviron/gortsplib/v4/pkg/description" 11 | "github.com/bluenviron/gortsplib/v4/pkg/format" 12 | ) 13 | 14 | func main() { 15 | if os.Getenv("MTX_QUERY") != "param=value" { 16 | panic("unexpected MTX_QUERY") 17 | } 18 | if os.Getenv("G1") != "on" { 19 | panic("unexpected G1") 20 | } 21 | 22 | medi := &description.Media{ 23 | Type: description.MediaTypeVideo, 24 | Formats: []format.Format{&format.H264{ 25 | PayloadTyp: 96, 26 | SPS: []byte{ 27 | 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, 28 | 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, 29 | 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, 30 | }, 31 | PPS: []byte{0x01, 0x02, 0x03, 0x04}, 32 | PacketizationMode: 1, 33 | }}, 34 | } 35 | 36 | source := gortsplib.Client{} 37 | 38 | err := source.StartRecording( 39 | "rtsp://localhost:"+os.Getenv("RTSP_PORT")+"/"+os.Getenv("MTX_PATH"), 40 | &description.Session{Medias: []*description.Media{medi}}) 41 | if err != nil { 42 | panic(err) 43 | } 44 | defer source.Close() 45 | 46 | c := make(chan os.Signal, 1) 47 | signal.Notify(c, syscall.SIGINT) 48 | <-c 49 | 50 | err = os.WriteFile(os.Getenv("ON_DEMAND"), []byte(""), 0o644) 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/counterdumper/counterdumper.go: -------------------------------------------------------------------------------- 1 | // Package counterdumper contains a counter that that periodically invokes a callback if the counter is not zero. 2 | package counterdumper 3 | 4 | import ( 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | const ( 10 | callbackPeriod = 1 * time.Second 11 | ) 12 | 13 | // CounterDumper is a counter that periodically invokes a callback if the counter is not zero. 14 | type CounterDumper struct { 15 | OnReport func(v uint64) 16 | 17 | counter *uint64 18 | 19 | terminate chan struct{} 20 | done chan struct{} 21 | } 22 | 23 | // Start starts the counter. 24 | func (c *CounterDumper) Start() { 25 | c.counter = new(uint64) 26 | c.terminate = make(chan struct{}) 27 | c.done = make(chan struct{}) 28 | 29 | go c.run() 30 | } 31 | 32 | // Stop stops the counter. 33 | func (c *CounterDumper) Stop() { 34 | close(c.terminate) 35 | <-c.done 36 | } 37 | 38 | // Increase increases the counter value by 1. 39 | func (c *CounterDumper) Increase() { 40 | atomic.AddUint64(c.counter, 1) 41 | } 42 | 43 | // Add adds value to the counter. 44 | func (c *CounterDumper) Add(v uint64) { 45 | atomic.AddUint64(c.counter, v) 46 | } 47 | 48 | func (c *CounterDumper) run() { 49 | defer close(c.done) 50 | 51 | t := time.NewTicker(callbackPeriod) 52 | defer t.Stop() 53 | 54 | for { 55 | select { 56 | case <-c.terminate: 57 | return 58 | 59 | case <-t.C: 60 | v := atomic.SwapUint64(c.counter, 0) 61 | if v != 0 { 62 | c.OnReport(v) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/counterdumper/counterdumper_test.go: -------------------------------------------------------------------------------- 1 | package counterdumper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCounterDumperReport(t *testing.T) { 11 | done := make(chan struct{}) 12 | 13 | c := &CounterDumper{ 14 | OnReport: func(v uint64) { 15 | require.Equal(t, uint64(3), v) 16 | close(done) 17 | }, 18 | } 19 | c.Start() 20 | defer c.Stop() 21 | 22 | c.Add(2) 23 | c.Increase() 24 | 25 | select { 26 | case <-done: 27 | case <-time.After(2 * time.Second): 28 | t.Errorf("should not happen") 29 | } 30 | } 31 | 32 | func TestCounterDumperDoNotReport(t *testing.T) { 33 | c := &CounterDumper{ 34 | OnReport: func(_ uint64) { 35 | t.Errorf("should not happen") 36 | }, 37 | } 38 | c.Start() 39 | defer c.Stop() 40 | 41 | <-time.After(2 * time.Second) 42 | } 43 | -------------------------------------------------------------------------------- /internal/defs/defs.go: -------------------------------------------------------------------------------- 1 | // Package defs contains shared definitions. 2 | package defs 3 | -------------------------------------------------------------------------------- /internal/defs/path_access_request.go: -------------------------------------------------------------------------------- 1 | package defs 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/bluenviron/mediamtx/internal/auth" 7 | "github.com/bluenviron/mediamtx/internal/conf" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // PathAccessRequest is a path access request. 12 | type PathAccessRequest struct { 13 | Name string 14 | Query string 15 | Publish bool 16 | SkipAuth bool 17 | 18 | // only if skipAuth = false 19 | Proto auth.Protocol 20 | ID *uuid.UUID 21 | Credentials *auth.Credentials 22 | IP net.IP 23 | CustomVerifyFunc func(expectedUser string, expectedPass string) bool 24 | } 25 | 26 | // ToAuthRequest converts a path access request into an authentication request. 27 | func (r *PathAccessRequest) ToAuthRequest() *auth.Request { 28 | return &auth.Request{ 29 | Action: func() conf.AuthAction { 30 | if r.Publish { 31 | return conf.AuthActionPublish 32 | } 33 | return conf.AuthActionRead 34 | }(), 35 | Path: r.Name, 36 | Query: r.Query, 37 | Protocol: r.Proto, 38 | ID: r.ID, 39 | Credentials: r.Credentials, 40 | IP: r.IP, 41 | CustomVerifyFunc: r.CustomVerifyFunc, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/defs/publisher.go: -------------------------------------------------------------------------------- 1 | package defs 2 | 3 | // Publisher is an entity that can publish a stream. 4 | type Publisher interface { 5 | Source 6 | Close() 7 | } 8 | -------------------------------------------------------------------------------- /internal/defs/reader.go: -------------------------------------------------------------------------------- 1 | package defs 2 | 3 | // Reader is an entity that can read a stream. 4 | type Reader interface { 5 | Close() 6 | APIReaderDescribe() APIPathSourceOrReader 7 | } 8 | -------------------------------------------------------------------------------- /internal/defs/source.go: -------------------------------------------------------------------------------- 1 | package defs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bluenviron/gortsplib/v4/pkg/description" 8 | "github.com/bluenviron/gortsplib/v4/pkg/format" 9 | 10 | "github.com/bluenviron/mediamtx/internal/logger" 11 | ) 12 | 13 | // Source is an entity that can provide a stream. 14 | // it can be: 15 | // - publisher 16 | // - staticsources.Handler 17 | // - redirectSource 18 | type Source interface { 19 | logger.Writer 20 | APISourceDescribe() APIPathSourceOrReader 21 | } 22 | 23 | // FormatsToCodecs returns the name of codecs of given formats. 24 | func FormatsToCodecs(formats []format.Format) []string { 25 | ret := make([]string, len(formats)) 26 | for i, forma := range formats { 27 | ret[i] = forma.Codec() 28 | } 29 | return ret 30 | } 31 | 32 | // FormatsInfo returns a description of formats. 33 | func FormatsInfo(formats []format.Format) string { 34 | return fmt.Sprintf("%d %s (%s)", 35 | len(formats), 36 | func() string { 37 | if len(formats) == 1 { 38 | return "track" 39 | } 40 | return "tracks" 41 | }(), 42 | strings.Join(FormatsToCodecs(formats), ", ")) 43 | } 44 | 45 | // MediasToCodecs returns the name of codecs of given formats. 46 | func MediasToCodecs(medias []*description.Media) []string { 47 | var formats []format.Format 48 | for _, media := range medias { 49 | formats = append(formats, media.Formats...) 50 | } 51 | 52 | return FormatsToCodecs(formats) 53 | } 54 | 55 | // MediasInfo returns a description of medias. 56 | func MediasInfo(medias []*description.Media) string { 57 | var formats []format.Format 58 | for _, media := range medias { 59 | formats = append(formats, media.Formats...) 60 | } 61 | 62 | return FormatsInfo(formats) 63 | } 64 | -------------------------------------------------------------------------------- /internal/defs/static_source.go: -------------------------------------------------------------------------------- 1 | package defs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/bluenviron/mediamtx/internal/conf" 7 | "github.com/bluenviron/mediamtx/internal/logger" 8 | ) 9 | 10 | // StaticSource is a static source. 11 | type StaticSource interface { 12 | logger.Writer 13 | Run(StaticSourceRunParams) error 14 | APISourceDescribe() APIPathSourceOrReader 15 | } 16 | 17 | // StaticSourceParent is the parent of a static source. 18 | type StaticSourceParent interface { 19 | logger.Writer 20 | SetReady(req PathSourceStaticSetReadyReq) PathSourceStaticSetReadyRes 21 | SetNotReady(req PathSourceStaticSetNotReadyReq) 22 | } 23 | 24 | // StaticSourceRunParams is the set of params passed to Run(). 25 | type StaticSourceRunParams struct { 26 | Context context.Context 27 | ResolvedSource string 28 | Conf *conf.Path 29 | ReloadConf chan *conf.Path 30 | } 31 | -------------------------------------------------------------------------------- /internal/externalcmd/cmd_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package externalcmd 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "syscall" 11 | 12 | "github.com/kballard/go-shellquote" 13 | ) 14 | 15 | func (e *Cmd) runOSSpecific(env []string) error { 16 | cmdParts, err := shellquote.Split(e.cmdstr) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | cmd := exec.Command(cmdParts[0], cmdParts[1:]...) 22 | 23 | cmd.Env = env 24 | cmd.Stdout = os.Stdout 25 | cmd.Stderr = os.Stderr 26 | 27 | // set process group in order to allow killing subprocesses 28 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 29 | 30 | err = cmd.Start() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | cmdDone := make(chan int) 36 | go func() { 37 | cmdDone <- func() int { 38 | err := cmd.Wait() 39 | if err == nil { 40 | return 0 41 | } 42 | var ee *exec.ExitError 43 | if errors.As(err, &ee) { 44 | ee.ExitCode() 45 | } 46 | return 0 47 | }() 48 | }() 49 | 50 | select { 51 | case <-e.terminate: 52 | // the minus is needed to kill all subprocesses 53 | syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) //nolint:errcheck 54 | <-cmdDone 55 | return errTerminated 56 | 57 | case c := <-cmdDone: 58 | if c != 0 { 59 | return fmt.Errorf("command exited with code %d", c) 60 | } 61 | return nil 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/externalcmd/pool.go: -------------------------------------------------------------------------------- 1 | package externalcmd 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Pool is a pool of external commands. 8 | type Pool struct { 9 | wg sync.WaitGroup 10 | } 11 | 12 | // Initialize initializes a Pool. 13 | func (p *Pool) Initialize() error { 14 | return nil 15 | } 16 | 17 | // Close waits for all external commands to exit. 18 | func (p *Pool) Close() { 19 | p.wg.Wait() 20 | } 21 | -------------------------------------------------------------------------------- /internal/formatprocessor/g711_test.go: -------------------------------------------------------------------------------- 1 | package formatprocessor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bluenviron/gortsplib/v4/pkg/format" 7 | "github.com/bluenviron/mediamtx/internal/unit" 8 | "github.com/pion/rtp" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestG711Encode(t *testing.T) { 13 | t.Run("alaw", func(t *testing.T) { 14 | forma := &format.G711{ 15 | PayloadTyp: 8, 16 | MULaw: false, 17 | SampleRate: 8000, 18 | ChannelCount: 1, 19 | } 20 | 21 | p, err := New(1472, forma, true, nil) 22 | require.NoError(t, err) 23 | 24 | unit := &unit.G711{ 25 | Samples: []byte{1, 2, 3, 4}, 26 | } 27 | 28 | err = p.ProcessUnit(unit) 29 | require.NoError(t, err) 30 | require.Equal(t, []*rtp.Packet{{ 31 | Header: rtp.Header{ 32 | Version: 2, 33 | PayloadType: 8, 34 | SequenceNumber: unit.RTPPackets[0].SequenceNumber, 35 | Timestamp: unit.RTPPackets[0].Timestamp, 36 | SSRC: unit.RTPPackets[0].SSRC, 37 | }, 38 | Payload: []byte{1, 2, 3, 4}, 39 | }}, unit.RTPPackets) 40 | }) 41 | 42 | t.Run("mulaw", func(t *testing.T) { 43 | forma := &format.G711{ 44 | PayloadTyp: 0, 45 | MULaw: true, 46 | SampleRate: 8000, 47 | ChannelCount: 1, 48 | } 49 | 50 | p, err := New(1472, forma, true, nil) 51 | require.NoError(t, err) 52 | 53 | unit := &unit.G711{ 54 | Samples: []byte{1, 2, 3, 4}, 55 | } 56 | 57 | err = p.ProcessUnit(unit) 58 | require.NoError(t, err) 59 | require.Equal(t, []*rtp.Packet{{ 60 | Header: rtp.Header{ 61 | Version: 2, 62 | PayloadType: 0, 63 | SequenceNumber: unit.RTPPackets[0].SequenceNumber, 64 | Timestamp: unit.RTPPackets[0].Timestamp, 65 | SSRC: unit.RTPPackets[0].SSRC, 66 | }, 67 | Payload: []byte{1, 2, 3, 4}, 68 | }}, unit.RTPPackets) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /internal/formatprocessor/generic.go: -------------------------------------------------------------------------------- 1 | package formatprocessor 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/bluenviron/gortsplib/v4/pkg/format" 8 | "github.com/pion/rtp" 9 | 10 | "github.com/bluenviron/mediamtx/internal/logger" 11 | "github.com/bluenviron/mediamtx/internal/unit" 12 | ) 13 | 14 | type generic struct { 15 | UDPMaxPayloadSize int 16 | Format format.Format 17 | GenerateRTPPackets bool 18 | Parent logger.Writer 19 | } 20 | 21 | func (t *generic) initialize() error { 22 | if t.GenerateRTPPackets { 23 | return fmt.Errorf("we don't know how to generate RTP packets of format %T", t.Format) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func (t *generic) ProcessUnit(_ unit.Unit) error { 30 | return fmt.Errorf("using a generic unit without RTP is not supported") 31 | } 32 | 33 | func (t *generic) ProcessRTPPacket( 34 | pkt *rtp.Packet, 35 | ntp time.Time, 36 | pts int64, 37 | _ bool, 38 | ) (unit.Unit, error) { 39 | u := &unit.Generic{ 40 | Base: unit.Base{ 41 | RTPPackets: []*rtp.Packet{pkt}, 42 | NTP: ntp, 43 | PTS: pts, 44 | }, 45 | } 46 | 47 | // remove padding 48 | pkt.Padding = false 49 | pkt.PaddingSize = 0 50 | 51 | if pkt.MarshalSize() > t.UDPMaxPayloadSize { 52 | return nil, fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)", 53 | pkt.MarshalSize(), t.UDPMaxPayloadSize) 54 | } 55 | 56 | return u, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/formatprocessor/generic_test.go: -------------------------------------------------------------------------------- 1 | package formatprocessor 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/bluenviron/gortsplib/v4/pkg/format" 8 | "github.com/pion/rtp" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGenericRemovePadding(t *testing.T) { 13 | forma := &format.Generic{ 14 | PayloadTyp: 96, 15 | RTPMa: "private/90000", 16 | } 17 | err := forma.Init() 18 | require.NoError(t, err) 19 | 20 | p, err := New(1472, forma, false, nil) 21 | require.NoError(t, err) 22 | 23 | pkt := &rtp.Packet{ 24 | Header: rtp.Header{ 25 | Version: 2, 26 | Marker: true, 27 | PayloadType: 96, 28 | SequenceNumber: 123, 29 | Timestamp: 45343, 30 | SSRC: 563423, 31 | Padding: true, 32 | }, 33 | Payload: []byte{1, 2, 3, 4}, 34 | PaddingSize: 20, 35 | } 36 | 37 | _, err = p.ProcessRTPPacket(pkt, time.Time{}, 0, false) 38 | require.NoError(t, err) 39 | 40 | require.Equal(t, &rtp.Packet{ 41 | Header: rtp.Header{ 42 | Version: 2, 43 | Marker: true, 44 | PayloadType: 96, 45 | SequenceNumber: 123, 46 | Timestamp: 45343, 47 | SSRC: 563423, 48 | }, 49 | Payload: []byte{1, 2, 3, 4}, 50 | }, pkt) 51 | } 52 | -------------------------------------------------------------------------------- /internal/formatprocessor/lpcm_test.go: -------------------------------------------------------------------------------- 1 | package formatprocessor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bluenviron/gortsplib/v4/pkg/format" 7 | "github.com/bluenviron/mediamtx/internal/unit" 8 | "github.com/pion/rtp" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLPCMEncode(t *testing.T) { 13 | forma := &format.LPCM{ 14 | PayloadTyp: 96, 15 | BitDepth: 16, 16 | SampleRate: 44100, 17 | ChannelCount: 2, 18 | } 19 | 20 | p, err := New(1472, forma, true, nil) 21 | require.NoError(t, err) 22 | 23 | unit := &unit.LPCM{ 24 | Samples: []byte{1, 2, 3, 4}, 25 | } 26 | 27 | err = p.ProcessUnit(unit) 28 | require.NoError(t, err) 29 | require.Equal(t, []*rtp.Packet{{ 30 | Header: rtp.Header{ 31 | Version: 2, 32 | PayloadType: 96, 33 | SequenceNumber: unit.RTPPackets[0].SequenceNumber, 34 | Timestamp: unit.RTPPackets[0].Timestamp, 35 | SSRC: unit.RTPPackets[0].SSRC, 36 | }, 37 | Payload: []byte{1, 2, 3, 4}, 38 | }}, unit.RTPPackets) 39 | } 40 | -------------------------------------------------------------------------------- /internal/formatprocessor/processor_test.go: -------------------------------------------------------------------------------- 1 | package formatprocessor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bluenviron/gortsplib/v4/pkg/format" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | for _, ca := range []struct { 12 | name string 13 | in format.Format 14 | out Processor 15 | }{ 16 | { 17 | "av1", 18 | &format.AV1{}, 19 | &av1{}, 20 | }, 21 | { 22 | "vp9", 23 | &format.VP9{}, 24 | &vp9{}, 25 | }, 26 | { 27 | "vp8", 28 | &format.VP8{}, 29 | &vp8{}, 30 | }, 31 | { 32 | "h265", 33 | &format.H265{}, 34 | &h265{}, 35 | }, 36 | { 37 | "h264", 38 | &format.H264{}, 39 | &h264{}, 40 | }, 41 | { 42 | "mpeg4 video", 43 | &format.MPEG4Video{}, 44 | &mpeg4Video{}, 45 | }, 46 | { 47 | "mpeg1 video", 48 | &format.MPEG1Video{}, 49 | &mpeg1Video{}, 50 | }, 51 | { 52 | "mpeg1 mjpeg", 53 | &format.MPEG1Audio{}, 54 | &mpeg1Audio{}, 55 | }, 56 | { 57 | "opus", 58 | &format.Opus{}, 59 | &opus{}, 60 | }, 61 | { 62 | "mpeg4 audio", 63 | &format.MPEG4Audio{}, 64 | &mpeg4Audio{}, 65 | }, 66 | { 67 | "mpeg1 audio", 68 | &format.MPEG1Audio{}, 69 | &mpeg1Audio{}, 70 | }, 71 | { 72 | "ac3", 73 | &format.AC3{}, 74 | &ac3{}, 75 | }, 76 | { 77 | "g711", 78 | &format.G711{}, 79 | &g711{}, 80 | }, 81 | { 82 | "lpcm", 83 | &format.LPCM{}, 84 | &lpcm{}, 85 | }, 86 | { 87 | "generic", 88 | &format.Generic{}, 89 | &generic{}, 90 | }, 91 | } { 92 | t.Run(ca.name, func(t *testing.T) { 93 | p, err := New(1472, ca.in, false, nil) 94 | require.NoError(t, err) 95 | require.IsType(t, ca.out, p) 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/048b606517c23baf: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("800") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/32e7782636603e29: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("8\x00\x00") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/caf81e9797b19c76: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/f428976a5b2917c0: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("80") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/353ba911ad2dc191: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("a00") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/3c3a72c00adac0b3: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("a0\x00\x00") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/c4389a565e828050: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("a000") 3 | -------------------------------------------------------------------------------- /internal/hooks/hooks.go: -------------------------------------------------------------------------------- 1 | // Package hooks contains hook implementations. 2 | package hooks 3 | -------------------------------------------------------------------------------- /internal/hooks/on_connect.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/bluenviron/mediamtx/internal/defs" 7 | "github.com/bluenviron/mediamtx/internal/externalcmd" 8 | "github.com/bluenviron/mediamtx/internal/logger" 9 | ) 10 | 11 | // OnConnectParams are the parameters of OnConnect. 12 | type OnConnectParams struct { 13 | Logger logger.Writer 14 | ExternalCmdPool *externalcmd.Pool 15 | RunOnConnect string 16 | RunOnConnectRestart bool 17 | RunOnDisconnect string 18 | RTSPAddress string 19 | Desc defs.APIPathSourceOrReader 20 | } 21 | 22 | // OnConnect is the OnConnect hook. 23 | func OnConnect(params OnConnectParams) func() { 24 | var env externalcmd.Environment 25 | var onConnectCmd *externalcmd.Cmd 26 | 27 | if params.RunOnConnect != "" || params.RunOnDisconnect != "" { 28 | _, port, _ := net.SplitHostPort(params.RTSPAddress) 29 | env = externalcmd.Environment{ 30 | "RTSP_PORT": port, 31 | "MTX_CONN_TYPE": params.Desc.Type, 32 | "MTX_CONN_ID": params.Desc.ID, 33 | } 34 | } 35 | 36 | if params.RunOnConnect != "" { 37 | params.Logger.Log(logger.Info, "runOnConnect command started") 38 | 39 | onConnectCmd = externalcmd.NewCmd( 40 | params.ExternalCmdPool, 41 | params.RunOnConnect, 42 | params.RunOnConnectRestart, 43 | env, 44 | func(err error) { 45 | params.Logger.Log(logger.Info, "runOnConnect command exited: %v", err) 46 | }) 47 | } 48 | 49 | return func() { 50 | if onConnectCmd != nil { 51 | onConnectCmd.Close() 52 | params.Logger.Log(logger.Info, "runOnConnect command stopped") 53 | } 54 | 55 | if params.RunOnDisconnect != "" { 56 | params.Logger.Log(logger.Info, "runOnDisconnect command launched") 57 | externalcmd.NewCmd( 58 | params.ExternalCmdPool, 59 | params.RunOnDisconnect, 60 | false, 61 | env, 62 | nil) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/hooks/on_demand.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/conf" 5 | "github.com/bluenviron/mediamtx/internal/externalcmd" 6 | "github.com/bluenviron/mediamtx/internal/logger" 7 | ) 8 | 9 | // OnDemandParams are the parameters of OnDemand. 10 | type OnDemandParams struct { 11 | Logger logger.Writer 12 | ExternalCmdPool *externalcmd.Pool 13 | Conf *conf.Path 14 | ExternalCmdEnv externalcmd.Environment 15 | Query string 16 | } 17 | 18 | // OnDemand is the OnDemand hook. 19 | func OnDemand(params OnDemandParams) func(string) { 20 | var env externalcmd.Environment 21 | var onDemandCmd *externalcmd.Cmd 22 | 23 | if params.Conf.RunOnDemand != "" || params.Conf.RunOnUnDemand != "" { 24 | env = params.ExternalCmdEnv 25 | env["MTX_QUERY"] = params.Query 26 | } 27 | 28 | if params.Conf.RunOnDemand != "" { 29 | params.Logger.Log(logger.Info, "runOnDemand command started") 30 | 31 | onDemandCmd = externalcmd.NewCmd( 32 | params.ExternalCmdPool, 33 | params.Conf.RunOnDemand, 34 | params.Conf.RunOnDemandRestart, 35 | env, 36 | func(err error) { 37 | params.Logger.Log(logger.Info, "runOnDemand command exited: %v", err) 38 | }) 39 | } 40 | 41 | return func(reason string) { 42 | if onDemandCmd != nil { 43 | onDemandCmd.Close() 44 | params.Logger.Log(logger.Info, "runOnDemand command stopped: %v", reason) 45 | } 46 | 47 | if params.Conf.RunOnUnDemand != "" { 48 | params.Logger.Log(logger.Info, "runOnUnDemand command launched") 49 | externalcmd.NewCmd( 50 | params.ExternalCmdPool, 51 | params.Conf.RunOnUnDemand, 52 | false, 53 | env, 54 | nil) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/hooks/on_init.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/conf" 5 | "github.com/bluenviron/mediamtx/internal/externalcmd" 6 | "github.com/bluenviron/mediamtx/internal/logger" 7 | ) 8 | 9 | // OnInitParams are the parameters of OnInit. 10 | type OnInitParams struct { 11 | Logger logger.Writer 12 | ExternalCmdPool *externalcmd.Pool 13 | Conf *conf.Path 14 | ExternalCmdEnv externalcmd.Environment 15 | } 16 | 17 | // OnInit is the OnInit hook. 18 | func OnInit(params OnInitParams) func() { 19 | var onInitCmd *externalcmd.Cmd 20 | 21 | if params.Conf.RunOnInit != "" { 22 | params.Logger.Log(logger.Info, "runOnInit command started") 23 | onInitCmd = externalcmd.NewCmd( 24 | params.ExternalCmdPool, 25 | params.Conf.RunOnInit, 26 | params.Conf.RunOnInitRestart, 27 | params.ExternalCmdEnv, 28 | func(err error) { 29 | params.Logger.Log(logger.Info, "runOnInit command exited: %v", err) 30 | }) 31 | } 32 | 33 | return func() { 34 | if onInitCmd != nil { 35 | onInitCmd.Close() 36 | params.Logger.Log(logger.Info, "runOnInit command stopped") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/hooks/on_read.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/conf" 5 | "github.com/bluenviron/mediamtx/internal/defs" 6 | "github.com/bluenviron/mediamtx/internal/externalcmd" 7 | "github.com/bluenviron/mediamtx/internal/logger" 8 | ) 9 | 10 | // OnReadParams are the parameters of OnRead. 11 | type OnReadParams struct { 12 | Logger logger.Writer 13 | ExternalCmdPool *externalcmd.Pool 14 | Conf *conf.Path 15 | ExternalCmdEnv externalcmd.Environment 16 | Reader defs.APIPathSourceOrReader 17 | Query string 18 | } 19 | 20 | // OnRead is the OnRead hook. 21 | func OnRead(params OnReadParams) func() { 22 | var env externalcmd.Environment 23 | var onReadCmd *externalcmd.Cmd 24 | 25 | if params.Conf.RunOnRead != "" || params.Conf.RunOnUnread != "" { 26 | env = params.ExternalCmdEnv 27 | desc := params.Reader 28 | env["MTX_QUERY"] = params.Query 29 | env["MTX_READER_TYPE"] = desc.Type 30 | env["MTX_READER_ID"] = desc.ID 31 | } 32 | 33 | if params.Conf.RunOnRead != "" { 34 | params.Logger.Log(logger.Info, "runOnRead command started") 35 | onReadCmd = externalcmd.NewCmd( 36 | params.ExternalCmdPool, 37 | params.Conf.RunOnRead, 38 | params.Conf.RunOnReadRestart, 39 | env, 40 | func(err error) { 41 | params.Logger.Log(logger.Info, "runOnRead command exited: %v", err) 42 | }) 43 | } 44 | 45 | return func() { 46 | if onReadCmd != nil { 47 | onReadCmd.Close() 48 | params.Logger.Log(logger.Info, "runOnRead command stopped") 49 | } 50 | 51 | if params.Conf.RunOnUnread != "" { 52 | params.Logger.Log(logger.Info, "runOnUnread command launched") 53 | externalcmd.NewCmd( 54 | params.ExternalCmdPool, 55 | params.Conf.RunOnUnread, 56 | false, 57 | env, 58 | nil) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/hooks/on_ready.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/conf" 5 | "github.com/bluenviron/mediamtx/internal/defs" 6 | "github.com/bluenviron/mediamtx/internal/externalcmd" 7 | "github.com/bluenviron/mediamtx/internal/logger" 8 | ) 9 | 10 | // OnReadyParams are the parameters of OnReady. 11 | type OnReadyParams struct { 12 | Logger logger.Writer 13 | ExternalCmdPool *externalcmd.Pool 14 | Conf *conf.Path 15 | ExternalCmdEnv externalcmd.Environment 16 | Desc defs.APIPathSourceOrReader 17 | Query string 18 | } 19 | 20 | // OnReady is the OnReady hook. 21 | func OnReady(params OnReadyParams) func() { 22 | var env externalcmd.Environment 23 | var onReadyCmd *externalcmd.Cmd 24 | 25 | if params.Conf.RunOnReady != "" || params.Conf.RunOnNotReady != "" { 26 | env = params.ExternalCmdEnv 27 | env["MTX_QUERY"] = params.Query 28 | env["MTX_SOURCE_TYPE"] = params.Desc.Type 29 | env["MTX_SOURCE_ID"] = params.Desc.ID 30 | } 31 | 32 | if params.Conf.RunOnReady != "" { 33 | params.Logger.Log(logger.Info, "runOnReady command started") 34 | onReadyCmd = externalcmd.NewCmd( 35 | params.ExternalCmdPool, 36 | params.Conf.RunOnReady, 37 | params.Conf.RunOnReadyRestart, 38 | env, 39 | func(err error) { 40 | params.Logger.Log(logger.Info, "runOnReady command exited: %v", err) 41 | }) 42 | } 43 | 44 | return func() { 45 | if onReadyCmd != nil { 46 | onReadyCmd.Close() 47 | params.Logger.Log(logger.Info, "runOnReady command stopped") 48 | } 49 | 50 | if params.Conf.RunOnNotReady != "" { 51 | params.Logger.Log(logger.Info, "runOnNotReady command launched") 52 | externalcmd.NewCmd( 53 | params.ExternalCmdPool, 54 | params.Conf.RunOnNotReady, 55 | false, 56 | env, 57 | nil) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/logger/destination.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Destination is a log destination. 8 | type Destination int 9 | 10 | const ( 11 | // DestinationStdout writes logs to the standard output. 12 | DestinationStdout Destination = iota 13 | 14 | // DestinationFile writes logs to a file. 15 | DestinationFile 16 | 17 | // DestinationSyslog writes logs to the system logger. 18 | DestinationSyslog 19 | ) 20 | 21 | type destination interface { 22 | log(time.Time, Level, string, ...interface{}) 23 | close() 24 | } 25 | -------------------------------------------------------------------------------- /internal/logger/destination_file.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "time" 7 | ) 8 | 9 | type destinationFile struct { 10 | file *os.File 11 | buf bytes.Buffer 12 | } 13 | 14 | func newDestinationFile(filePath string) (destination, error) { 15 | f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &destinationFile{ 21 | file: f, 22 | }, nil 23 | } 24 | 25 | func (d *destinationFile) log(t time.Time, level Level, format string, args ...interface{}) { 26 | d.buf.Reset() 27 | writeTime(&d.buf, t, false) 28 | writeLevel(&d.buf, level, false) 29 | writeContent(&d.buf, format, args) 30 | d.file.Write(d.buf.Bytes()) //nolint:errcheck 31 | } 32 | 33 | func (d *destinationFile) close() { 34 | d.file.Close() 35 | } 36 | -------------------------------------------------------------------------------- /internal/logger/destination_stdout.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "time" 7 | 8 | "golang.org/x/term" 9 | ) 10 | 11 | type destinationStdout struct { 12 | useColor bool 13 | 14 | buf bytes.Buffer 15 | } 16 | 17 | func newDestionationStdout() destination { 18 | return &destinationStdout{ 19 | useColor: term.IsTerminal(int(os.Stdout.Fd())), 20 | } 21 | } 22 | 23 | func (d *destinationStdout) log(t time.Time, level Level, format string, args ...interface{}) { 24 | d.buf.Reset() 25 | writeTime(&d.buf, t, d.useColor) 26 | writeLevel(&d.buf, level, d.useColor) 27 | writeContent(&d.buf, format, args) 28 | os.Stdout.Write(d.buf.Bytes()) //nolint:errcheck 29 | } 30 | 31 | func (d *destinationStdout) close() { 32 | } 33 | -------------------------------------------------------------------------------- /internal/logger/destination_syslog.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "time" 7 | ) 8 | 9 | type destinationSysLog struct { 10 | syslog io.WriteCloser 11 | buf bytes.Buffer 12 | } 13 | 14 | func newDestinationSyslog(prefix string) (destination, error) { 15 | syslog, err := newSysLog(prefix) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &destinationSysLog{ 21 | syslog: syslog, 22 | }, nil 23 | } 24 | 25 | func (d *destinationSysLog) log(t time.Time, level Level, format string, args ...interface{}) { 26 | d.buf.Reset() 27 | writeTime(&d.buf, t, false) 28 | writeLevel(&d.buf, level, false) 29 | writeContent(&d.buf, format, args) 30 | d.syslog.Write(d.buf.Bytes()) //nolint:errcheck 31 | } 32 | 33 | func (d *destinationSysLog) close() { 34 | d.syslog.Close() 35 | } 36 | -------------------------------------------------------------------------------- /internal/logger/level.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // Level is a log level. 4 | type Level int 5 | 6 | // Log levels. 7 | const ( 8 | Debug Level = iota + 1 9 | Info 10 | Warn 11 | Error 12 | ) 13 | -------------------------------------------------------------------------------- /internal/logger/syslog_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package logger 4 | 5 | import ( 6 | "io" 7 | native "log/syslog" 8 | ) 9 | 10 | type syslog struct { 11 | inner *native.Writer 12 | } 13 | 14 | func newSysLog(prefix string) (io.WriteCloser, error) { 15 | inner, err := native.New(native.LOG_INFO|native.LOG_DAEMON, prefix) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &syslog{ 21 | inner: inner, 22 | }, nil 23 | } 24 | 25 | func (ls *syslog) Close() error { 26 | return ls.inner.Close() 27 | } 28 | 29 | func (ls *syslog) Write(p []byte) (int, error) { 30 | return ls.inner.Write(p) 31 | } 32 | -------------------------------------------------------------------------------- /internal/logger/syslog_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package logger 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | func newSysLog(prefix string) (io.WriteCloser, error) { 11 | return nil, fmt.Errorf("not implemented on windows") 12 | } 13 | -------------------------------------------------------------------------------- /internal/logger/writer.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // Writer is an object that provides a log method. 4 | type Writer interface { 5 | Log(Level, string, ...interface{}) 6 | } 7 | -------------------------------------------------------------------------------- /internal/metrics/metrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/bluenviron/mediamtx/internal/conf" 10 | "github.com/bluenviron/mediamtx/internal/test" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestPreflightRequest(t *testing.T) { 15 | api := Metrics{ 16 | Address: "localhost:9998", 17 | AllowOrigin: "*", 18 | ReadTimeout: conf.Duration(10 * time.Second), 19 | AuthManager: test.NilAuthManager, 20 | Parent: test.NilLogger, 21 | } 22 | err := api.Initialize() 23 | require.NoError(t, err) 24 | defer api.Close() 25 | 26 | tr := &http.Transport{} 27 | defer tr.CloseIdleConnections() 28 | hc := &http.Client{Transport: tr} 29 | 30 | req, err := http.NewRequest(http.MethodOptions, "http://localhost:9998", nil) 31 | require.NoError(t, err) 32 | 33 | req.Header.Add("Access-Control-Request-Method", "GET") 34 | 35 | res, err := hc.Do(req) 36 | require.NoError(t, err) 37 | defer res.Body.Close() 38 | 39 | require.Equal(t, http.StatusNoContent, res.StatusCode) 40 | 41 | byts, err := io.ReadAll(res.Body) 42 | require.NoError(t, err) 43 | 44 | require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) 45 | require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) 46 | require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) 47 | require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) 48 | require.Equal(t, byts, []byte{}) 49 | } 50 | -------------------------------------------------------------------------------- /internal/playback/muxer.go: -------------------------------------------------------------------------------- 1 | package playback 2 | 3 | import "github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4" 4 | 5 | type muxer interface { 6 | writeInit(init *fmp4.Init) 7 | setTrack(trackID int) 8 | writeSample( 9 | dts int64, 10 | ptsOffset int32, 11 | isNonSyncSample bool, 12 | payloadSize uint32, 13 | getPayload func() ([]byte, error), 14 | ) error 15 | writeFinalDTS(dts int64) 16 | flush() error 17 | } 18 | -------------------------------------------------------------------------------- /internal/playback/segment_fmp4_test.go: -------------------------------------------------------------------------------- 1 | package playback 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" 9 | "github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4" 10 | "github.com/bluenviron/mediacommon/v2/pkg/formats/mp4" 11 | "github.com/bluenviron/mediamtx/internal/test" 12 | ) 13 | 14 | func writeBenchInit(f io.WriteSeeker) { 15 | init := fmp4.Init{ 16 | Tracks: []*fmp4.InitTrack{ 17 | { 18 | ID: 1, 19 | TimeScale: 90000, 20 | Codec: &mp4.CodecH264{ 21 | SPS: test.FormatH264.SPS, 22 | PPS: test.FormatH264.PPS, 23 | }, 24 | }, 25 | { 26 | ID: 2, 27 | TimeScale: 90000, 28 | Codec: &mp4.CodecMPEG4Audio{ 29 | Config: mpeg4audio.Config{ 30 | Type: mpeg4audio.ObjectTypeAACLC, 31 | SampleRate: 48000, 32 | ChannelCount: 2, 33 | }, 34 | }, 35 | }, 36 | }, 37 | } 38 | 39 | err := init.Marshal(f) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | _, err = f.Write([]byte{ 45 | 0x00, 0x00, 0x00, 0x10, 'm', 'o', 'o', 'f', 46 | }) 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | func BenchmarkFMP4ReadHeader(b *testing.B) { 53 | f, err := os.CreateTemp(os.TempDir(), "mediamtx-playback-fmp4-") 54 | if err != nil { 55 | panic(err) 56 | } 57 | defer os.Remove(f.Name()) 58 | 59 | writeBenchInit(f) 60 | f.Close() 61 | 62 | for n := 0; n < b.N; n++ { 63 | func() { 64 | f, err = os.Open(f.Name()) 65 | if err != nil { 66 | panic(err) 67 | } 68 | defer f.Close() 69 | 70 | _, _, err = segmentFMP4ReadHeader(f) 71 | if err != nil { 72 | panic(err) 73 | } 74 | }() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/playback/server_test.go: -------------------------------------------------------------------------------- 1 | package playback 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/bluenviron/mediamtx/internal/conf" 10 | "github.com/bluenviron/mediamtx/internal/test" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestPreflightRequest(t *testing.T) { 15 | s := &Server{ 16 | Address: "127.0.0.1:9996", 17 | AllowOrigin: "*", 18 | ReadTimeout: conf.Duration(10 * time.Second), 19 | Parent: test.NilLogger, 20 | } 21 | err := s.Initialize() 22 | require.NoError(t, err) 23 | defer s.Close() 24 | 25 | tr := &http.Transport{} 26 | defer tr.CloseIdleConnections() 27 | hc := &http.Client{Transport: tr} 28 | 29 | req, err := http.NewRequest(http.MethodOptions, "http://localhost:9996", nil) 30 | require.NoError(t, err) 31 | 32 | req.Header.Add("Access-Control-Request-Method", "GET") 33 | 34 | res, err := hc.Do(req) 35 | require.NoError(t, err) 36 | defer res.Body.Close() 37 | 38 | require.Equal(t, http.StatusNoContent, res.StatusCode) 39 | 40 | byts, err := io.ReadAll(res.Body) 41 | require.NoError(t, err) 42 | 43 | require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) 44 | require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) 45 | require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) 46 | require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) 47 | require.Equal(t, byts, []byte{}) 48 | } 49 | -------------------------------------------------------------------------------- /internal/protocols/httpp/content_type.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import "strings" 4 | 5 | // ParseContentType parses a Content-Type header and returns the content type. 6 | func ParseContentType(v string) string { 7 | return strings.TrimSpace(strings.Split(v, ";")[0]) 8 | } 9 | -------------------------------------------------------------------------------- /internal/protocols/httpp/credentials.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/bluenviron/mediamtx/internal/auth" 8 | ) 9 | 10 | // Credentials extracts credentials from a HTTP request. 11 | func Credentials(h *http.Request) *auth.Credentials { 12 | c := &auth.Credentials{} 13 | 14 | for _, auth := range h.Header["Authorization"] { 15 | if strings.HasPrefix(auth, "Bearer ") { 16 | // user:pass in Authorization Bearer 17 | if parts := strings.Split(auth[len("Bearer "):], ":"); len(parts) == 2 { 18 | c.User = parts[0] 19 | c.Pass = parts[1] 20 | return c 21 | } 22 | 23 | // JWT in Authorization Bearer 24 | c.Token = auth[len("Bearer "):] 25 | return c 26 | } 27 | } 28 | 29 | // user:pass in Authorization Basic 30 | c.User, c.Pass, _ = h.BasicAuth() 31 | 32 | return c 33 | } 34 | -------------------------------------------------------------------------------- /internal/protocols/httpp/credentials_test.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/bluenviron/mediamtx/internal/auth" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCredentials(t *testing.T) { 13 | t.Run("user and pass in basic", func(t *testing.T) { 14 | h := &http.Request{ 15 | URL: &url.URL{}, 16 | Header: http.Header{ 17 | "Authorization": []string{ 18 | "Basic bXl1c2VyOm15cGFzcw==", 19 | }, 20 | }, 21 | } 22 | 23 | c := Credentials(h) 24 | 25 | require.Equal(t, &auth.Credentials{ 26 | User: "myuser", 27 | Pass: "mypass", 28 | }, c) 29 | }) 30 | 31 | t.Run("user and pass in bearer", func(t *testing.T) { 32 | h := &http.Request{ 33 | URL: &url.URL{}, 34 | Header: http.Header{ 35 | "Authorization": []string{ 36 | "Bearer myuser:mypass", 37 | }, 38 | }, 39 | } 40 | 41 | c := Credentials(h) 42 | 43 | require.Equal(t, &auth.Credentials{ 44 | User: "myuser", 45 | Pass: "mypass", 46 | }, c) 47 | }) 48 | 49 | t.Run("token in bearer", func(t *testing.T) { 50 | h := &http.Request{ 51 | URL: &url.URL{}, 52 | Header: http.Header{ 53 | "Authorization": []string{ 54 | "Bearer testing123", 55 | }, 56 | }, 57 | } 58 | 59 | c := Credentials(h) 60 | 61 | require.Equal(t, &auth.Credentials{ 62 | Token: "testing123", 63 | }, c) 64 | }) 65 | 66 | t.Run("user and pass and token", func(t *testing.T) { 67 | h := &http.Request{ 68 | URL: &url.URL{}, 69 | Header: http.Header{ 70 | "Authorization": []string{ 71 | "Basic bXl1c2VyOm15cGFzcw==", 72 | "Bearer testing123", 73 | }, 74 | }, 75 | } 76 | 77 | c := Credentials(h) 78 | 79 | require.Equal(t, &auth.Credentials{ 80 | Token: "testing123", 81 | }, c) 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /internal/protocols/httpp/handler_exit_on_panic.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | ) 9 | 10 | // exit when there's a panic inside the HTTP handler. 11 | // https://github.com/golang/go/issues/16542 12 | type handlerExitOnPanic struct { 13 | http.Handler 14 | } 15 | 16 | func (h *handlerExitOnPanic) ServeHTTP(w http.ResponseWriter, r *http.Request) { 17 | defer func() { 18 | err := recover() 19 | if err != nil { 20 | buf := make([]byte, 1<<20) 21 | n := runtime.Stack(buf, true) 22 | fmt.Fprintf(os.Stderr, "panic: %v\n\n%s", err, buf[:n]) 23 | os.Exit(1) 24 | } 25 | }() 26 | h.Handler.ServeHTTP(w, r) 27 | } 28 | -------------------------------------------------------------------------------- /internal/protocols/httpp/handler_filter_requests.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // reject requests with empty paths. 8 | type handlerFilterRequests struct { 9 | http.Handler 10 | } 11 | 12 | func (h *handlerFilterRequests) ServeHTTP(w http.ResponseWriter, r *http.Request) { 13 | if r.URL.Path == "" || r.URL.Path[0] != '/' { 14 | w.WriteHeader(http.StatusBadRequest) 15 | return 16 | } 17 | h.Handler.ServeHTTP(w, r) 18 | } 19 | -------------------------------------------------------------------------------- /internal/protocols/httpp/handler_logger.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httputil" 8 | 9 | "github.com/bluenviron/mediamtx/internal/logger" 10 | ) 11 | 12 | type loggerWriter struct { 13 | w http.ResponseWriter 14 | status int 15 | buf bytes.Buffer 16 | } 17 | 18 | func (w *loggerWriter) Header() http.Header { 19 | return w.w.Header() 20 | } 21 | 22 | func (w *loggerWriter) Write(b []byte) (int, error) { 23 | if w.status == 0 { 24 | w.status = http.StatusOK 25 | } 26 | w.buf.Write(b) 27 | return w.w.Write(b) 28 | } 29 | 30 | func (w *loggerWriter) WriteHeader(statusCode int) { 31 | w.status = statusCode 32 | w.w.WriteHeader(statusCode) 33 | } 34 | 35 | func (w *loggerWriter) dump() string { 36 | var buf bytes.Buffer 37 | fmt.Fprintf(&buf, "%s %d %s\n", "HTTP/1.1", w.status, http.StatusText(w.status)) 38 | w.w.Header().Write(&buf) //nolint:errcheck 39 | buf.Write([]byte("\n")) 40 | if w.buf.Len() > 0 { 41 | fmt.Fprintf(&buf, "(body of %d bytes)", w.buf.Len()) 42 | } 43 | return buf.String() 44 | } 45 | 46 | // log requests and responses. 47 | type handlerLogger struct { 48 | http.Handler 49 | log logger.Writer 50 | } 51 | 52 | func (h *handlerLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { 53 | byts, _ := httputil.DumpRequest(r, true) 54 | h.log.Log(logger.Debug, "[conn %v] [c->s] %s", r.RemoteAddr, string(byts)) 55 | 56 | logw := &loggerWriter{w: w} 57 | 58 | h.Handler.ServeHTTP(logw, r) 59 | 60 | h.log.Log(logger.Debug, "[conn %v] [s->c] %s", r.RemoteAddr, logw.dump()) 61 | } 62 | -------------------------------------------------------------------------------- /internal/protocols/httpp/handler_server_header.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // set the Server header. 8 | type handlerServerHeader struct { 9 | http.Handler 10 | } 11 | 12 | func (h *handlerServerHeader) ServeHTTP(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Server", "mediamtx") 14 | h.Handler.ServeHTTP(w, r) 15 | } 16 | -------------------------------------------------------------------------------- /internal/protocols/httpp/remote_addr.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // RemoteAddr returns the remote address of an HTTP client, 10 | // with the IP replaced by the real IP passed by any proxy in between. 11 | func RemoteAddr(ctx *gin.Context) string { 12 | ip := ctx.ClientIP() 13 | _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) 14 | return net.JoinHostPort(ip, port) 15 | } 16 | -------------------------------------------------------------------------------- /internal/protocols/httpp/server_test.go: -------------------------------------------------------------------------------- 1 | package httpp 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/bluenviron/mediamtx/internal/test" 12 | ) 13 | 14 | func TestFilterEmptyPath(t *testing.T) { 15 | s := &Server{ 16 | Network: "tcp", 17 | Address: "localhost:4555", 18 | ReadTimeout: 10 * time.Second, 19 | Parent: test.NilLogger, 20 | } 21 | err := s.Initialize() 22 | require.NoError(t, err) 23 | defer s.Close() 24 | 25 | conn, err := net.Dial("tcp", "localhost:4555") 26 | require.NoError(t, err) 27 | defer conn.Close() 28 | 29 | _, err = conn.Write([]byte("OPTIONS http://localhost HTTP/1.1\n" + 30 | "Host: localhost:8889\n" + 31 | "Accept-Encoding: gzip\n" + 32 | "User-Agent: Go-http-client/1.1\n\n")) 33 | require.NoError(t, err) 34 | 35 | buf := make([]byte, 20) 36 | _, err = io.ReadFull(conn, buf) 37 | require.NoError(t, err) 38 | } 39 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/object.go: -------------------------------------------------------------------------------- 1 | package amf0 2 | 3 | // ObjectEntry is an entry of Object. 4 | type ObjectEntry struct { 5 | Key string 6 | Value interface{} 7 | } 8 | 9 | // Object is an AMF0 object. 10 | type Object []ObjectEntry 11 | 12 | // ECMAArray is an AMF0 ECMA Array. 13 | type ECMAArray Object 14 | 15 | // Get returns the value corresponding to key. 16 | func (o Object) Get(key string) (interface{}, bool) { 17 | for _, item := range o { 18 | if item.Key == key { 19 | return item.Value, true 20 | } 21 | } 22 | return nil, false 23 | } 24 | 25 | // GetString returns the value corresponding to key, only if that is a string. 26 | func (o Object) GetString(key string) (string, bool) { 27 | v, ok := o.Get(key) 28 | if !ok { 29 | return "", false 30 | } 31 | 32 | v2, ok2 := v.(string) 33 | if !ok2 { 34 | return "", false 35 | } 36 | 37 | return v2, ok2 38 | } 39 | 40 | // GetFloat64 returns the value corresponding to key, only if that is a float64. 41 | func (o Object) GetFloat64(key string) (float64, bool) { 42 | v, ok := o.Get(key) 43 | if !ok { 44 | return 0, false 45 | } 46 | 47 | v2, ok2 := v.(float64) 48 | if !ok2 { 49 | return 0, false 50 | } 51 | 52 | return v2, ok2 53 | } 54 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/object_test.go: -------------------------------------------------------------------------------- 1 | package amf0 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestObjectGet(t *testing.T) { 10 | o := Object{{Key: "testme", Value: "ok"}} 11 | v, ok := o.Get("testme") 12 | require.Equal(t, true, ok) 13 | require.Equal(t, "ok", v) 14 | } 15 | 16 | func TestObjectGetString(t *testing.T) { 17 | o := Object{{Key: "testme", Value: "ok"}} 18 | v, ok := o.GetString("testme") 19 | require.Equal(t, true, ok) 20 | require.Equal(t, "ok", v) 21 | } 22 | 23 | func TestObjectGetFloat64(t *testing.T) { 24 | o := Object{{Key: "testme", Value: float64(123)}} 25 | v, ok := o.GetFloat64("testme") 26 | require.Equal(t, true, ok) 27 | require.Equal(t, float64(123), v) 28 | } 29 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/110121ffaa6941a6: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x0200") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/118a6dec0931d635: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/13ba6ce8ecfbc991: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b0000\x00\x000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/399a2041db7e1ff9: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/6df12bd1a096b953: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x03\x00\x0200") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/6f9c29532ccb80bf: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/7fbe967ee430d9f4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x0300") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/97dc7172b48e6ffd: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x01") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/aba8f2fdaa5caedb: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x03\x00\x000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/b27b930eec286237: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\n0000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/b9ef082bd4d297b2: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b0000\x00\x00") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/de35ed0e8078c6ef: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\n") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/df015416c2cf2dc6: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b0000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/e93e775e4de86c93: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\b0000\x00\x06000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/f3b49d384cddc291: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x03") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/amf0/testdata/fuzz/FuzzUnmarshal/fb46839f39edbbd8: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x03\x00\x00") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/bytecounter/reader.go: -------------------------------------------------------------------------------- 1 | package bytecounter 2 | 3 | import ( 4 | "io" 5 | "sync/atomic" 6 | ) 7 | 8 | // Reader allows to count read bytes. 9 | type Reader struct { 10 | r io.Reader 11 | count uint64 12 | } 13 | 14 | // NewReader allocates a Reader. 15 | func NewReader(r io.Reader) *Reader { 16 | return &Reader{ 17 | r: r, 18 | } 19 | } 20 | 21 | // Read implements io.Reader. 22 | func (r *Reader) Read(p []byte) (int, error) { 23 | n, err := r.r.Read(p) 24 | atomic.AddUint64(&r.count, uint64(n)) 25 | return n, err 26 | } 27 | 28 | // Count returns received bytes. 29 | func (r *Reader) Count() uint64 { 30 | return atomic.LoadUint64(&r.count) 31 | } 32 | 33 | // SetCount sets read bytes. 34 | func (r *Reader) SetCount(v uint64) { 35 | atomic.StoreUint64(&r.count, v) 36 | } 37 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/bytecounter/reader_test.go: -------------------------------------------------------------------------------- 1 | package bytecounter 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReader(t *testing.T) { 11 | var buf bytes.Buffer 12 | buf.Write(bytes.Repeat([]byte{0x01}, 1024)) 13 | 14 | r := NewReader(&buf) 15 | r.SetCount(100) 16 | 17 | buf2 := make([]byte, 64) 18 | n, err := r.Read(buf2) 19 | require.NoError(t, err) 20 | require.Equal(t, 64, n) 21 | 22 | require.Equal(t, uint64(100+64), r.Count()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/bytecounter/readwriter.go: -------------------------------------------------------------------------------- 1 | // Package bytecounter contains a reader/writer that allows to count bytes. 2 | package bytecounter 3 | 4 | import ( 5 | "io" 6 | ) 7 | 8 | // ReadWriter allows to count read and written bytes. 9 | type ReadWriter struct { 10 | *Reader 11 | *Writer 12 | } 13 | 14 | // NewReadWriter allocates a ReadWriter. 15 | func NewReadWriter(rw io.ReadWriter) *ReadWriter { 16 | return &ReadWriter{ 17 | Reader: NewReader(rw), 18 | Writer: NewWriter(rw), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/bytecounter/writer.go: -------------------------------------------------------------------------------- 1 | package bytecounter 2 | 3 | import ( 4 | "io" 5 | "sync/atomic" 6 | ) 7 | 8 | // Writer allows to count written bytes. 9 | type Writer struct { 10 | w io.Writer 11 | count uint64 12 | } 13 | 14 | // NewWriter allocates a Writer. 15 | func NewWriter(w io.Writer) *Writer { 16 | return &Writer{ 17 | w: w, 18 | } 19 | } 20 | 21 | // Write implements io.Writer. 22 | func (w *Writer) Write(p []byte) (int, error) { 23 | n, err := w.w.Write(p) 24 | atomic.AddUint64(&w.count, uint64(n)) 25 | return n, err 26 | } 27 | 28 | // Count returns sent bytes. 29 | func (w *Writer) Count() uint64 { 30 | return atomic.LoadUint64(&w.count) 31 | } 32 | 33 | // SetCount sets sent bytes. 34 | func (w *Writer) SetCount(v uint64) { 35 | atomic.StoreUint64(&w.count, v) 36 | } 37 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/bytecounter/writer_test.go: -------------------------------------------------------------------------------- 1 | package bytecounter 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWriter(t *testing.T) { 11 | var buf bytes.Buffer 12 | 13 | w := NewWriter(&buf) 14 | w.SetCount(100) 15 | 16 | _, err := w.Write(bytes.Repeat([]byte{0x01}, 64)) 17 | require.NoError(t, err) 18 | require.Equal(t, uint64(100+64), w.Count()) 19 | } 20 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/chunk.go: -------------------------------------------------------------------------------- 1 | // Package chunk implements RTMP chunks. 2 | package chunk 3 | 4 | import ( 5 | "io" 6 | ) 7 | 8 | // Chunk is a chunk. 9 | type Chunk interface { 10 | Read(r io.Reader, bodyLen uint32, hasExtendedTimestamp bool) error 11 | Marshal(hasExtendedTimestamp bool) ([]byte, error) 12 | } 13 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/chunk2.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Chunk2 is a type 2 chunk. 8 | // Neither the stream ID nor the 9 | // message length is included; this chunk has the same stream ID and 10 | // message length as the preceding chunk. 11 | type Chunk2 struct { 12 | ChunkStreamID byte 13 | TimestampDelta uint32 14 | Body []byte 15 | } 16 | 17 | // Read reads the chunk. 18 | func (c *Chunk2) Read(r io.Reader, bodyLen uint32, _ bool) error { 19 | header := make([]byte, 4) 20 | _, err := io.ReadFull(r, header) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | c.ChunkStreamID = header[0] & 0x3F 26 | c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3]) 27 | 28 | if c.TimestampDelta >= 0xFFFFFF { 29 | _, err = io.ReadFull(r, header[:4]) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | c.TimestampDelta = uint32(header[0])<<24 | uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3]) 35 | } 36 | 37 | c.Body = make([]byte, bodyLen) 38 | _, err = io.ReadFull(r, c.Body) 39 | return err 40 | } 41 | 42 | func (c Chunk2) marshalSize() int { 43 | n := 4 + len(c.Body) 44 | if c.TimestampDelta >= 0xFFFFFF { 45 | n += 4 46 | } 47 | return n 48 | } 49 | 50 | // Marshal writes the chunk. 51 | func (c Chunk2) Marshal(_ bool) ([]byte, error) { 52 | buf := make([]byte, c.marshalSize()) 53 | buf[0] = 2<<6 | c.ChunkStreamID 54 | 55 | if c.TimestampDelta >= 0xFFFFFF { 56 | buf[1] = 0xFF 57 | buf[2] = 0xFF 58 | buf[3] = 0xFF 59 | buf[4] = byte(c.TimestampDelta >> 24) 60 | buf[5] = byte(c.TimestampDelta >> 16) 61 | buf[6] = byte(c.TimestampDelta >> 8) 62 | buf[7] = byte(c.TimestampDelta) 63 | copy(buf[8:], c.Body) 64 | } else { 65 | buf[1] = byte(c.TimestampDelta >> 16) 66 | buf[2] = byte(c.TimestampDelta >> 8) 67 | buf[3] = byte(c.TimestampDelta) 68 | copy(buf[4:], c.Body) 69 | } 70 | 71 | return buf, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/chunk3.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Chunk3 is a type 3 chunk. 8 | // Type 3 chunks have no message header. The stream ID, message length 9 | // and timestamp delta fields are not present; chunks of this type take 10 | // values from the preceding chunk for the same Chunk Stream ID. When a 11 | // single message is split into chunks, all chunks of a message except 12 | // the first one SHOULD use this type. 13 | type Chunk3 struct { 14 | ChunkStreamID byte 15 | Body []byte 16 | } 17 | 18 | // Read reads the chunk. 19 | func (c *Chunk3) Read(r io.Reader, bodyLen uint32, hasExtendedTimestamp bool) error { 20 | header := make([]byte, 4) 21 | _, err := io.ReadFull(r, header[:1]) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | c.ChunkStreamID = header[0] & 0x3F 27 | 28 | if hasExtendedTimestamp { 29 | _, err = io.ReadFull(r, header[:4]) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | c.Body = make([]byte, bodyLen) 36 | _, err = io.ReadFull(r, c.Body) 37 | return err 38 | } 39 | 40 | func (c Chunk3) marshalSize(hasExtendedTimestamp bool) int { 41 | n := 1 + len(c.Body) 42 | if hasExtendedTimestamp { 43 | n += 4 44 | } 45 | return n 46 | } 47 | 48 | // Marshal writes the chunk. 49 | func (c Chunk3) Marshal(hasExtendedTimestamp bool) ([]byte, error) { 50 | buf := make([]byte, c.marshalSize(hasExtendedTimestamp)) 51 | buf[0] = 3<<6 | c.ChunkStreamID 52 | 53 | if hasExtendedTimestamp { 54 | copy(buf[5:], c.Body) 55 | } else { 56 | copy(buf[1:], c.Body) 57 | } 58 | 59 | return buf, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/5f73a77c7f93e5f8: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0\xff\xff\xff00000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/553384c8664fe971: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0\xff\xff\xff0000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/feb2b2a8b4ba63ba: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0\xff\xff\xff") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/582528ddfad69eb5: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/caf81e9797b19c76: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/conn.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter" 7 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message" 8 | ) 9 | 10 | // Conn is implemented by Client and ServerConn. 11 | type Conn interface { 12 | BytesReceived() uint64 13 | BytesSent() uint64 14 | Read() (message.Message, error) 15 | Write(msg message.Message) error 16 | } 17 | 18 | type dummyConn struct { 19 | rw io.ReadWriter 20 | 21 | bc *bytecounter.ReadWriter 22 | mrw *message.ReadWriter 23 | } 24 | 25 | func (c *dummyConn) initialize() { 26 | c.bc = bytecounter.NewReadWriter(c.rw) 27 | c.mrw = message.NewReadWriter(c.bc, c.bc, false) 28 | } 29 | 30 | // BytesReceived returns the number of bytes received. 31 | func (c *dummyConn) BytesReceived() uint64 { 32 | return c.bc.Reader.Count() 33 | } 34 | 35 | // BytesSent returns the number of bytes sent. 36 | func (c *dummyConn) BytesSent() uint64 { 37 | return c.bc.Writer.Count() 38 | } 39 | 40 | func (c *dummyConn) Read() (message.Message, error) { 41 | return c.mrw.Read() 42 | } 43 | 44 | func (c *dummyConn) Write(msg message.Message) error { 45 | return c.mrw.Write(msg) 46 | } 47 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/h264conf/h264conf.go: -------------------------------------------------------------------------------- 1 | // Package h264conf contains a H264 configuration parser. 2 | package h264conf 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // Conf is a RTMP H264 configuration. 9 | type Conf struct { 10 | SPS []byte 11 | PPS []byte 12 | } 13 | 14 | // Unmarshal decodes a Conf from bytes. 15 | func (c *Conf) Unmarshal(buf []byte) error { 16 | if len(buf) < 8 { 17 | return fmt.Errorf("invalid size 1") 18 | } 19 | 20 | pos := 5 21 | 22 | spsCount := buf[pos] & 0x1F 23 | pos++ 24 | if spsCount != 1 { 25 | return fmt.Errorf("sps count != 1 is unsupported") 26 | } 27 | 28 | spsLen := int(uint16(buf[pos])<<8 | uint16(buf[pos+1])) 29 | pos += 2 30 | if (len(buf) - pos) < spsLen { 31 | return fmt.Errorf("invalid size 2") 32 | } 33 | 34 | c.SPS = buf[pos : pos+spsLen] 35 | pos += spsLen 36 | 37 | if (len(buf) - pos) < 3 { 38 | return fmt.Errorf("invalid size 3") 39 | } 40 | 41 | ppsCount := buf[pos] 42 | pos++ 43 | if ppsCount != 1 { 44 | return fmt.Errorf("pps count != 1 is unsupported") 45 | } 46 | 47 | ppsLen := int(uint16(buf[pos])<<8 | uint16(buf[pos+1])) 48 | pos += 2 49 | if (len(buf) - pos) < ppsLen { 50 | return fmt.Errorf("invalid size") 51 | } 52 | 53 | c.PPS = buf[pos : pos+ppsLen] 54 | 55 | return nil 56 | } 57 | 58 | // Marshal encodes a Conf into bytes. 59 | func (c Conf) Marshal() ([]byte, error) { 60 | spsLen := len(c.SPS) 61 | ppsLen := len(c.PPS) 62 | 63 | buf := make([]byte, 11+spsLen+ppsLen) 64 | 65 | buf[0] = 1 66 | buf[1] = c.SPS[1] 67 | buf[2] = c.SPS[2] 68 | buf[3] = c.SPS[3] 69 | buf[4] = 3 | 0xFC 70 | buf[5] = 1 | 0xE0 71 | pos := 6 72 | 73 | buf[pos] = byte(spsLen >> 8) 74 | buf[pos+1] = byte(spsLen) 75 | pos += 2 76 | 77 | copy(buf[pos:], c.SPS) 78 | pos += spsLen 79 | 80 | buf[pos] = 1 81 | pos++ 82 | 83 | buf[pos] = byte(ppsLen >> 8) 84 | buf[pos+1] = byte(ppsLen) 85 | pos += 2 86 | 87 | copy(buf[pos:], c.PPS) 88 | 89 | return buf, nil 90 | } 91 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/h264conf/h264conf_test.go: -------------------------------------------------------------------------------- 1 | package h264conf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | var decoded = Conf{ 10 | SPS: []byte{0x45, 0x32, 0xA3, 0x08}, 11 | PPS: []byte{0x45, 0x34}, 12 | } 13 | 14 | var encoded = []byte{ 15 | 0x1, 0x32, 0xa3, 0x8, 0xff, 0xe1, 0x0, 0x4, 0x45, 0x32, 0xa3, 0x8, 0x1, 0x0, 0x2, 0x45, 0x34, 16 | } 17 | 18 | func TestUnmarshal(t *testing.T) { 19 | var dec Conf 20 | err := dec.Unmarshal(encoded) 21 | require.NoError(t, err) 22 | require.Equal(t, decoded, dec) 23 | } 24 | 25 | func TestMarshal(t *testing.T) { 26 | enc, err := decoded.Marshal() 27 | require.NoError(t, err) 28 | require.Equal(t, encoded, enc) 29 | } 30 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/handshake/c0s0.go: -------------------------------------------------------------------------------- 1 | package handshake 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // C0S0 is a C0 or S0 packet. 9 | type C0S0 struct { 10 | Version byte 11 | } 12 | 13 | // Read reads a C0S0. 14 | func (c *C0S0) Read(r io.Reader) error { 15 | buf := make([]byte, 1) 16 | _, err := io.ReadFull(r, buf) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | c.Version = buf[0] 22 | 23 | if c.Version != 3 && c.Version != 6 { 24 | return fmt.Errorf("invalid rtmp version (%d)", c.Version) 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // Write writes a C0S0. 31 | func (c C0S0) Write(w io.Writer) error { 32 | _, err := w.Write([]byte{c.Version}) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/handshake/c0s0_test.go: -------------------------------------------------------------------------------- 1 | package handshake 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var c0s0enc = []byte{3} 11 | 12 | var c0s0dec = C0S0{ 13 | Version: 3, 14 | } 15 | 16 | func TestC0S0Read(t *testing.T) { 17 | var c0s0 C0S0 18 | err := c0s0.Read((bytes.NewReader(c0s0enc))) 19 | require.NoError(t, err) 20 | require.Equal(t, c0s0dec, c0s0) 21 | } 22 | 23 | func TestC0S0Write(t *testing.T) { 24 | var buf bytes.Buffer 25 | err := c0s0dec.Write(&buf) 26 | require.NoError(t, err) 27 | require.Equal(t, c0s0enc, buf.Bytes()) 28 | } 29 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/handshake/c1s1_test.go: -------------------------------------------------------------------------------- 1 | package handshake 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var c1s1enc = bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 1536/4) 11 | 12 | var c1s1dec = C1S1{ 13 | Time: 16909060, 14 | Version: 16909060, 15 | Data: bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 1536/4-2), 16 | } 17 | 18 | func TestC1S1Read(t *testing.T) { 19 | var c1s1 C1S1 20 | err := c1s1.Read((bytes.NewReader(c1s1enc))) 21 | require.NoError(t, err) 22 | require.Equal(t, c1s1dec, c1s1) 23 | } 24 | 25 | func TestC1S1Write(t *testing.T) { 26 | var buf bytes.Buffer 27 | err := c1s1dec.Write(&buf) 28 | require.NoError(t, err) 29 | require.Equal(t, c1s1enc, buf.Bytes()) 30 | } 31 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/handshake/c2s2_test.go: -------------------------------------------------------------------------------- 1 | package handshake 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var c2s2enc = bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 1536/4) 11 | 12 | var c2s2dec = C2S2{ 13 | Time: 16909060, 14 | Time2: 16909060, 15 | Data: bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 1536/4-2), 16 | } 17 | 18 | func TestC2S2Read(t *testing.T) { 19 | var c2s2 C2S2 20 | err := c2s2.Read((bytes.NewReader(c2s2enc))) 21 | require.NoError(t, err) 22 | require.Equal(t, c2s2dec, c2s2) 23 | } 24 | 25 | func TestC2S2Write(t *testing.T) { 26 | var buf bytes.Buffer 27 | err := c2s2dec.Write(&buf) 28 | require.NoError(t, err) 29 | require.Equal(t, c2s2enc, buf.Bytes()) 30 | } 31 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/handshake/handshake_test.go: -------------------------------------------------------------------------------- 1 | package handshake 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | type testReadWriter struct { 10 | ch chan []byte 11 | } 12 | 13 | func (rw *testReadWriter) Read(p []byte) (int, error) { 14 | in := <-rw.ch 15 | n := copy(p, in) 16 | return n, nil 17 | } 18 | 19 | func (rw *testReadWriter) Write(p []byte) (int, error) { 20 | rw.ch <- p 21 | return len(p), nil 22 | } 23 | 24 | func TestHandshake(t *testing.T) { 25 | for _, ca := range []string{"plain", "encrypted"} { 26 | t.Run(ca, func(t *testing.T) { 27 | rw := &testReadWriter{ch: make(chan []byte)} 28 | var serverInKey []byte 29 | var serverOutKey []byte 30 | done := make(chan struct{}) 31 | 32 | go func() { 33 | var err error 34 | serverInKey, serverOutKey, err = DoServer(rw, true) 35 | require.NoError(t, err) 36 | close(done) 37 | }() 38 | 39 | clientInKey, clientOutKey, err := DoClient(rw, ca == "encrypted", true) 40 | require.NoError(t, err) 41 | 42 | <-done 43 | 44 | if ca == "encrypted" { 45 | require.NotNil(t, serverInKey) 46 | require.Equal(t, serverInKey, clientOutKey) 47 | require.Equal(t, serverOutKey, clientInKey) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_acknowledge.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // Acknowledge is an acknowledgement message. 10 | type Acknowledge struct { 11 | Value uint32 12 | } 13 | 14 | func (m *Acknowledge) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 4 { 20 | return fmt.Errorf("unexpected body size") 21 | } 22 | 23 | m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) 24 | 25 | return nil 26 | } 27 | 28 | func (m *Acknowledge) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 4) 30 | 31 | buf[0] = byte(m.Value >> 24) 32 | buf[1] = byte(m.Value >> 16) 33 | buf[2] = byte(m.Value >> 8) 34 | buf[3] = byte(m.Value) 35 | 36 | return &rawmessage.Message{ 37 | ChunkStreamID: ControlChunkStreamID, 38 | Type: uint8(TypeAcknowledge), 39 | Body: buf, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_audio_ex_coded_frames.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 8 | ) 9 | 10 | // AudioExCodedFrames is a CodedFrames extended message. 11 | type AudioExCodedFrames struct { 12 | ChunkStreamID byte 13 | DTS time.Duration 14 | MessageStreamID uint32 15 | FourCC FourCC 16 | Payload []byte 17 | } 18 | 19 | func (m *AudioExCodedFrames) unmarshal(raw *rawmessage.Message) error { 20 | if len(raw.Body) < 5 { 21 | return fmt.Errorf("not enough bytes") 22 | } 23 | 24 | m.ChunkStreamID = raw.ChunkStreamID 25 | m.DTS = raw.Timestamp 26 | m.MessageStreamID = raw.MessageStreamID 27 | 28 | m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4]) 29 | switch m.FourCC { 30 | case FourCCOpus, FourCCAC3, FourCCMP4A, FourCCMP3: 31 | default: 32 | return fmt.Errorf("unsupported fourCC: %v", m.FourCC) 33 | } 34 | 35 | m.Payload = raw.Body[5:] 36 | 37 | return nil 38 | } 39 | 40 | func (m AudioExCodedFrames) marshalBodySize() int { 41 | return 5 + len(m.Payload) 42 | } 43 | 44 | func (m AudioExCodedFrames) marshal() (*rawmessage.Message, error) { 45 | body := make([]byte, m.marshalBodySize()) 46 | 47 | body[0] = (9 << 4) | byte(AudioExTypeCodedFrames) 48 | body[1] = uint8(m.FourCC >> 24) 49 | body[2] = uint8(m.FourCC >> 16) 50 | body[3] = uint8(m.FourCC >> 8) 51 | body[4] = uint8(m.FourCC) 52 | copy(body[5:], m.Payload) 53 | 54 | return &rawmessage.Message{ 55 | ChunkStreamID: m.ChunkStreamID, 56 | Timestamp: m.DTS, 57 | Type: uint8(TypeAudio), 58 | MessageStreamID: m.MessageStreamID, 59 | Body: body, 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_audio_ex_sequence_end.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // AudioExSequenceEnd is a sequence end extended message. 10 | type AudioExSequenceEnd struct { 11 | ChunkStreamID byte 12 | MessageStreamID uint32 13 | FourCC FourCC 14 | } 15 | 16 | func (m *AudioExSequenceEnd) unmarshal(raw *rawmessage.Message) error { 17 | if len(raw.Body) != 5 { 18 | return fmt.Errorf("not enough bytes") 19 | } 20 | 21 | m.ChunkStreamID = raw.ChunkStreamID 22 | m.MessageStreamID = raw.MessageStreamID 23 | 24 | m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4]) 25 | switch m.FourCC { 26 | case FourCCOpus, FourCCAC3, FourCCMP4A, FourCCMP3: 27 | default: 28 | return fmt.Errorf("unsupported fourCC: %v", m.FourCC) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (m AudioExSequenceEnd) marshal() (*rawmessage.Message, error) { 35 | body := make([]byte, 5) 36 | 37 | body[0] = (9 << 4) | byte(AudioExTypeSequenceEnd) 38 | body[1] = uint8(m.FourCC >> 24) 39 | body[2] = uint8(m.FourCC >> 16) 40 | body[3] = uint8(m.FourCC >> 8) 41 | body[4] = uint8(m.FourCC) 42 | 43 | return &rawmessage.Message{ 44 | ChunkStreamID: m.ChunkStreamID, 45 | Type: uint8(TypeAudio), 46 | MessageStreamID: m.MessageStreamID, 47 | Body: body, 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_command_amf0.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/amf0" 7 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 8 | ) 9 | 10 | // CommandAMF0 is a AMF0 command message. 11 | type CommandAMF0 struct { 12 | ChunkStreamID byte 13 | MessageStreamID uint32 14 | Name string 15 | CommandID int 16 | Arguments amf0.Data 17 | } 18 | 19 | func (m *CommandAMF0) unmarshal(raw *rawmessage.Message) error { 20 | m.ChunkStreamID = raw.ChunkStreamID 21 | m.MessageStreamID = raw.MessageStreamID 22 | 23 | payload, err := amf0.Unmarshal(raw.Body) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if len(payload) < 3 { 29 | return fmt.Errorf("invalid command payload") 30 | } 31 | 32 | var ok bool 33 | m.Name, ok = payload[0].(string) 34 | if !ok { 35 | return fmt.Errorf("invalid command payload") 36 | } 37 | 38 | tmp, ok := payload[1].(float64) 39 | if !ok { 40 | return fmt.Errorf("invalid command payload") 41 | } 42 | m.CommandID = int(tmp) 43 | 44 | m.Arguments = payload[2:] 45 | 46 | return nil 47 | } 48 | 49 | func (m CommandAMF0) marshal() (*rawmessage.Message, error) { 50 | data := append(amf0.Data{ 51 | m.Name, 52 | float64(m.CommandID), 53 | }, m.Arguments...) 54 | 55 | body, err := data.Marshal() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &rawmessage.Message{ 61 | ChunkStreamID: m.ChunkStreamID, 62 | Type: uint8(TypeCommandAMF0), 63 | MessageStreamID: m.MessageStreamID, 64 | Body: body, 65 | }, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_data_amf0.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/amf0" 5 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 6 | ) 7 | 8 | // DataAMF0 is a AMF0 data message. 9 | type DataAMF0 struct { 10 | ChunkStreamID byte 11 | MessageStreamID uint32 12 | Payload amf0.Data 13 | } 14 | 15 | func (m *DataAMF0) unmarshal(raw *rawmessage.Message) error { 16 | m.ChunkStreamID = raw.ChunkStreamID 17 | m.MessageStreamID = raw.MessageStreamID 18 | 19 | var err error 20 | m.Payload, err = amf0.Unmarshal(raw.Body) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (m DataAMF0) marshal() (*rawmessage.Message, error) { 29 | body, err := m.Payload.Marshal() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &rawmessage.Message{ 35 | ChunkStreamID: m.ChunkStreamID, 36 | Type: uint8(TypeDataAMF0), 37 | MessageStreamID: m.MessageStreamID, 38 | Body: body, 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_set_chunk_size.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // SetChunkSize is a set chunk size message. 10 | type SetChunkSize struct { 11 | Value uint32 12 | } 13 | 14 | func (m *SetChunkSize) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 4 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) 24 | 25 | return nil 26 | } 27 | 28 | func (m *SetChunkSize) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 4) 30 | 31 | buf[0] = byte(m.Value >> 24) 32 | buf[1] = byte(m.Value >> 16) 33 | buf[2] = byte(m.Value >> 8) 34 | buf[3] = byte(m.Value) 35 | 36 | return &rawmessage.Message{ 37 | ChunkStreamID: ControlChunkStreamID, 38 | Type: uint8(TypeSetChunkSize), 39 | Body: buf, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_set_peer_bandwidth.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // SetPeerBandwidth is a set peer bandwidth message. 10 | type SetPeerBandwidth struct { 11 | Value uint32 12 | Type byte 13 | } 14 | 15 | func (m *SetPeerBandwidth) unmarshal(raw *rawmessage.Message) error { 16 | if raw.ChunkStreamID != ControlChunkStreamID { 17 | return fmt.Errorf("unexpected chunk stream ID") 18 | } 19 | 20 | if len(raw.Body) != 5 { 21 | return fmt.Errorf("invalid body size") 22 | } 23 | 24 | m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) 25 | m.Type = raw.Body[4] 26 | 27 | return nil 28 | } 29 | 30 | func (m *SetPeerBandwidth) marshal() (*rawmessage.Message, error) { 31 | buf := make([]byte, 5) 32 | 33 | buf[0] = byte(m.Value >> 24) 34 | buf[1] = byte(m.Value >> 16) 35 | buf[2] = byte(m.Value >> 8) 36 | buf[3] = byte(m.Value) 37 | buf[4] = m.Type 38 | 39 | return &rawmessage.Message{ 40 | ChunkStreamID: ControlChunkStreamID, 41 | Type: uint8(TypeSetPeerBandwidth), 42 | Body: buf, 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_set_window_ack_size.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // SetWindowAckSize is a set window acknowledgement message. 10 | type SetWindowAckSize struct { 11 | Value uint32 12 | } 13 | 14 | func (m *SetWindowAckSize) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 4 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.Value = uint32(raw.Body[0])<<24 | uint32(raw.Body[1])<<16 | uint32(raw.Body[2])<<8 | uint32(raw.Body[3]) 24 | 25 | return nil 26 | } 27 | 28 | func (m *SetWindowAckSize) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 4) 30 | 31 | buf[0] = byte(m.Value >> 24) 32 | buf[1] = byte(m.Value >> 16) 33 | buf[2] = byte(m.Value >> 8) 34 | buf[3] = byte(m.Value) 35 | 36 | return &rawmessage.Message{ 37 | ChunkStreamID: ControlChunkStreamID, 38 | Type: uint8(TypeSetWindowAckSize), 39 | Body: buf, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_ping_request.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlPingRequest is a user control message. 10 | type UserControlPingRequest struct { 11 | ServerTime uint32 12 | } 13 | 14 | func (m *UserControlPingRequest) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.ServerTime = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlPingRequest) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypePingRequest >> 8) 32 | buf[1] = byte(UserControlTypePingRequest) 33 | buf[2] = byte(m.ServerTime >> 24) 34 | buf[3] = byte(m.ServerTime >> 16) 35 | buf[4] = byte(m.ServerTime >> 8) 36 | buf[5] = byte(m.ServerTime) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_ping_response.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlPingResponse is a user control message. 10 | type UserControlPingResponse struct { 11 | ServerTime uint32 12 | } 13 | 14 | func (m *UserControlPingResponse) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.ServerTime = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlPingResponse) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypePingResponse >> 8) 32 | buf[1] = byte(UserControlTypePingResponse) 33 | buf[2] = byte(m.ServerTime >> 24) 34 | buf[3] = byte(m.ServerTime >> 16) 35 | buf[4] = byte(m.ServerTime >> 8) 36 | buf[5] = byte(m.ServerTime) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_set_buffer_length.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlSetBufferLength is a user control message. 10 | type UserControlSetBufferLength struct { 11 | StreamID uint32 12 | BufferLength uint32 13 | } 14 | 15 | func (m *UserControlSetBufferLength) unmarshal(raw *rawmessage.Message) error { 16 | if raw.ChunkStreamID != ControlChunkStreamID { 17 | return fmt.Errorf("unexpected chunk stream ID") 18 | } 19 | 20 | if len(raw.Body) != 10 { 21 | return fmt.Errorf("invalid body size") 22 | } 23 | 24 | m.StreamID = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 25 | m.BufferLength = uint32(raw.Body[6])<<24 | uint32(raw.Body[7])<<16 | uint32(raw.Body[8])<<8 | uint32(raw.Body[9]) 26 | 27 | return nil 28 | } 29 | 30 | func (m UserControlSetBufferLength) marshal() (*rawmessage.Message, error) { 31 | buf := make([]byte, 10) 32 | 33 | buf[0] = byte(UserControlTypeSetBufferLength >> 8) 34 | buf[1] = byte(UserControlTypeSetBufferLength) 35 | buf[2] = byte(m.StreamID >> 24) 36 | buf[3] = byte(m.StreamID >> 16) 37 | buf[4] = byte(m.StreamID >> 8) 38 | buf[5] = byte(m.StreamID) 39 | buf[6] = byte(m.BufferLength >> 24) 40 | buf[7] = byte(m.BufferLength >> 16) 41 | buf[8] = byte(m.BufferLength >> 8) 42 | buf[9] = byte(m.BufferLength) 43 | 44 | return &rawmessage.Message{ 45 | ChunkStreamID: ControlChunkStreamID, 46 | Type: uint8(TypeUserControl), 47 | Body: buf, 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_stream_begin.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlStreamBegin is a user control message. 10 | type UserControlStreamBegin struct { 11 | StreamID uint32 12 | } 13 | 14 | func (m *UserControlStreamBegin) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.StreamID = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlStreamBegin) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypeStreamBegin >> 8) 32 | buf[1] = byte(UserControlTypeStreamBegin) 33 | buf[2] = byte(m.StreamID >> 24) 34 | buf[3] = byte(m.StreamID >> 16) 35 | buf[4] = byte(m.StreamID >> 8) 36 | buf[5] = byte(m.StreamID) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_stream_dry.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlStreamDry is a user control message. 10 | type UserControlStreamDry struct { 11 | StreamID uint32 12 | } 13 | 14 | func (m *UserControlStreamDry) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.StreamID = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlStreamDry) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypeStreamDry >> 8) 32 | buf[1] = byte(UserControlTypeStreamDry) 33 | buf[2] = byte(m.StreamID >> 24) 34 | buf[3] = byte(m.StreamID >> 16) 35 | buf[4] = byte(m.StreamID >> 8) 36 | buf[5] = byte(m.StreamID) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_stream_eof.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlStreamEOF is a user control message. 10 | type UserControlStreamEOF struct { 11 | StreamID uint32 12 | } 13 | 14 | func (m *UserControlStreamEOF) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.StreamID = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlStreamEOF) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypeStreamEOF >> 8) 32 | buf[1] = byte(UserControlTypeStreamEOF) 33 | buf[2] = byte(m.StreamID >> 24) 34 | buf[3] = byte(m.StreamID >> 16) 35 | buf[4] = byte(m.StreamID >> 8) 36 | buf[5] = byte(m.StreamID) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_user_control_stream_is_recorded.go: -------------------------------------------------------------------------------- 1 | package message //nolint:dupl 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // UserControlStreamIsRecorded is a user control message. 10 | type UserControlStreamIsRecorded struct { 11 | StreamID uint32 12 | } 13 | 14 | func (m *UserControlStreamIsRecorded) unmarshal(raw *rawmessage.Message) error { 15 | if raw.ChunkStreamID != ControlChunkStreamID { 16 | return fmt.Errorf("unexpected chunk stream ID") 17 | } 18 | 19 | if len(raw.Body) != 6 { 20 | return fmt.Errorf("invalid body size") 21 | } 22 | 23 | m.StreamID = uint32(raw.Body[2])<<24 | uint32(raw.Body[3])<<16 | uint32(raw.Body[4])<<8 | uint32(raw.Body[5]) 24 | 25 | return nil 26 | } 27 | 28 | func (m UserControlStreamIsRecorded) marshal() (*rawmessage.Message, error) { 29 | buf := make([]byte, 6) 30 | 31 | buf[0] = byte(UserControlTypeStreamIsRecorded >> 8) 32 | buf[1] = byte(UserControlTypeStreamIsRecorded) 33 | buf[2] = byte(m.StreamID >> 24) 34 | buf[3] = byte(m.StreamID >> 16) 35 | buf[4] = byte(m.StreamID >> 8) 36 | buf[5] = byte(m.StreamID) 37 | 38 | return &rawmessage.Message{ 39 | ChunkStreamID: ControlChunkStreamID, 40 | Type: uint8(TypeUserControl), 41 | Body: buf, 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_video_ex_frames_x.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 8 | ) 9 | 10 | // VideoExFramesX is a FramesX extended message. 11 | type VideoExFramesX struct { 12 | ChunkStreamID byte 13 | DTS time.Duration 14 | MessageStreamID uint32 15 | FourCC FourCC 16 | Payload []byte 17 | } 18 | 19 | func (m *VideoExFramesX) unmarshal(raw *rawmessage.Message) error { 20 | if len(raw.Body) < 6 { 21 | return fmt.Errorf("not enough bytes") 22 | } 23 | 24 | m.ChunkStreamID = raw.ChunkStreamID 25 | m.DTS = raw.Timestamp 26 | m.MessageStreamID = raw.MessageStreamID 27 | 28 | m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4]) 29 | switch m.FourCC { 30 | case FourCCAV1, FourCCVP9, FourCCHEVC, FourCCAVC: 31 | default: 32 | return fmt.Errorf("unsupported fourCC: %v", m.FourCC) 33 | } 34 | 35 | m.Payload = raw.Body[5:] 36 | 37 | return nil 38 | } 39 | 40 | func (m VideoExFramesX) marshalBodySize() int { 41 | return 5 + len(m.Payload) 42 | } 43 | 44 | func (m VideoExFramesX) marshal() (*rawmessage.Message, error) { 45 | body := make([]byte, m.marshalBodySize()) 46 | 47 | body[0] = 0b10000000 | byte(VideoExTypeFramesX) 48 | body[1] = uint8(m.FourCC >> 24) 49 | body[2] = uint8(m.FourCC >> 16) 50 | body[3] = uint8(m.FourCC >> 8) 51 | body[4] = uint8(m.FourCC) 52 | copy(body[5:], m.Payload) 53 | 54 | return &rawmessage.Message{ 55 | ChunkStreamID: m.ChunkStreamID, 56 | Timestamp: m.DTS, 57 | Type: uint8(TypeVideo), 58 | MessageStreamID: m.MessageStreamID, 59 | Body: body, 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/msg_video_ex_sequence_end.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 7 | ) 8 | 9 | // VideoExSequenceEnd is a sequence end extended message. 10 | type VideoExSequenceEnd struct { 11 | ChunkStreamID byte 12 | MessageStreamID uint32 13 | FourCC FourCC 14 | } 15 | 16 | func (m *VideoExSequenceEnd) unmarshal(raw *rawmessage.Message) error { 17 | if len(raw.Body) != 5 { 18 | return fmt.Errorf("not enough bytes") 19 | } 20 | 21 | m.ChunkStreamID = raw.ChunkStreamID 22 | m.MessageStreamID = raw.MessageStreamID 23 | 24 | m.FourCC = FourCC(raw.Body[1])<<24 | FourCC(raw.Body[2])<<16 | FourCC(raw.Body[3])<<8 | FourCC(raw.Body[4]) 25 | switch m.FourCC { 26 | case FourCCAV1, FourCCVP9, FourCCHEVC, FourCCAVC: 27 | default: 28 | return fmt.Errorf("unsupported fourCC: %v", m.FourCC) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (m VideoExSequenceEnd) marshalBodySize() int { 35 | return 5 36 | } 37 | 38 | func (m VideoExSequenceEnd) marshal() (*rawmessage.Message, error) { 39 | body := make([]byte, m.marshalBodySize()) 40 | 41 | body[0] = 0b10000000 | byte(VideoExTypeSequenceEnd) 42 | body[1] = uint8(m.FourCC >> 24) 43 | body[2] = uint8(m.FourCC >> 16) 44 | body[3] = uint8(m.FourCC >> 8) 45 | body[4] = uint8(m.FourCC) 46 | 47 | return &rawmessage.Message{ 48 | ChunkStreamID: m.ChunkStreamID, 49 | Type: uint8(TypeVideo), 50 | MessageStreamID: m.MessageStreamID, 51 | Body: body, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/readwriter.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter" 7 | ) 8 | 9 | // ReadWriter is a message reader/writer. 10 | type ReadWriter struct { 11 | r *Reader 12 | w *Writer 13 | } 14 | 15 | // NewReadWriter allocates a ReadWriter. 16 | func NewReadWriter( 17 | rw io.ReadWriter, 18 | bcrw *bytecounter.ReadWriter, 19 | checkAcknowledge bool, 20 | ) *ReadWriter { 21 | w := NewWriter(rw, bcrw.Writer, checkAcknowledge) 22 | 23 | r := NewReader(rw, bcrw.Reader, func(count uint32) error { 24 | return w.Write(&Acknowledge{ 25 | Value: count, 26 | }) 27 | }) 28 | 29 | return &ReadWriter{ 30 | r: r, 31 | w: w, 32 | } 33 | } 34 | 35 | // Read reads a message. 36 | func (rw *ReadWriter) Read() (Message, error) { 37 | msg, err := rw.r.Read() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | switch tmsg := msg.(type) { 43 | case *Acknowledge: 44 | rw.w.SetAcknowledgeValue(tmsg.Value) 45 | 46 | case *UserControlPingRequest: 47 | err := rw.w.Write(&UserControlPingResponse{ 48 | ServerTime: tmsg.ServerTime, 49 | }) 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | 55 | return msg, nil 56 | } 57 | 58 | // Write writes a message. 59 | func (rw *ReadWriter) Write(msg Message) error { 60 | return rw.w.Write(msg) 61 | } 62 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/readwriter_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter" 11 | ) 12 | 13 | type duplexRW struct { 14 | io.Reader 15 | io.Writer 16 | } 17 | 18 | func (d *duplexRW) Read(p []byte) (int, error) { 19 | return d.Reader.Read(p) 20 | } 21 | 22 | func (d *duplexRW) Write(p []byte) (int, error) { 23 | return d.Writer.Write(p) 24 | } 25 | 26 | func TestReadWriterAcknowledge(t *testing.T) { 27 | var buf1 bytes.Buffer 28 | var buf2 bytes.Buffer 29 | 30 | bc1 := bytecounter.NewReadWriter(&duplexRW{ 31 | Reader: &buf2, 32 | Writer: &buf1, 33 | }) 34 | rw1 := NewReadWriter(bc1, bc1, true) 35 | err := rw1.Write(&Acknowledge{ 36 | Value: 7863534, 37 | }) 38 | require.NoError(t, err) 39 | 40 | bc2 := bytecounter.NewReadWriter(&duplexRW{ 41 | Reader: &buf1, 42 | Writer: &buf2, 43 | }) 44 | rw2 := NewReadWriter(bc2, bc2, true) 45 | _, err = rw2.Read() 46 | require.NoError(t, err) 47 | } 48 | 49 | func TestReadWriterPing(t *testing.T) { 50 | var buf1 bytes.Buffer 51 | var buf2 bytes.Buffer 52 | 53 | bc1 := bytecounter.NewReadWriter(&duplexRW{ 54 | Reader: &buf2, 55 | Writer: &buf1, 56 | }) 57 | rw1 := NewReadWriter(bc1, bc1, true) 58 | err := rw1.Write(&UserControlPingRequest{ 59 | ServerTime: 143424312, 60 | }) 61 | require.NoError(t, err) 62 | 63 | bc2 := bytecounter.NewReadWriter(&duplexRW{ 64 | Reader: &buf1, 65 | Writer: &buf2, 66 | }) 67 | rw2 := NewReadWriter(bc2, bc2, true) 68 | _, err = rw2.Read() 69 | require.NoError(t, err) 70 | 71 | msg, err := rw1.Read() 72 | require.NoError(t, err) 73 | require.Equal(t, &UserControlPingResponse{ 74 | ServerTime: 143424312, 75 | }, msg) 76 | } 77 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/05172fb3869a3972: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\f\b0000\x90mp4a0000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/05d2521061b772dd: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/05d9ba0f9aad1a7f: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x18\b0000\x90Opus0000000000000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/06f5bdb4e0ba6885: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\f\x120000\f\x00\x00\x00\x010000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/105635bd94dfe048: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\x94hvc1000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/158d13fe96bff3cf: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\x810000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/21f6cf04358dfa71: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\a\b0000\x94mp4a00") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/23ef7989663ea23d: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\v\b0000\x910000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/2a18e30b05c133c9: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x02\b0000\x900") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/2fb5da434799f2aa: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x00\t0000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/31a26997abe34698: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xa6\x00000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/35202254b97bc326: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\x81av01") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/373e7702328587a8: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\b0000\x90Opus000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/378ec2a143cd2612: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x03\t0000\x9100") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/39f38a6fb2bbbbb3: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\x81avc1") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/3bcc52bfc7a4dab7: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x18\b0000\x9000000000000000000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/420fac969d79c3d0: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\f\x040000000000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/5805dd8ebdbd7064: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xb30000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/64e5c42dd1ecb0c2: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xa6\x03000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/6b1d357b508b38a4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\v\t0000\xe50000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/6f2dcd363bea1d98: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x95\x0f00000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/6f557bbcba30c3e8: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x18\b0000\x90OpusOpusHead00000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/857f71dbea87237e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\x940000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/898db8bfed216016: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x95\x0400000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/8a81d89bb8b52d87: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\v\b0000\x94Opus\x0000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/904ada19512a3bc4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\a\b0000\x94mp4a\x010") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/920c0b072d6284a2: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x95\x0100000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/96fcc4dc967cb84a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xf6\x01000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/9a651b61e58fe16f: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x95000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/9f06d9ce342f9e6e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xf6\x02000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/a016402385fe2568: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\a\b0000\x94mp4a\x020") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/a392a07f6f19c310: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\b0000\x920000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/b18a01392c7c91e9: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x0000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/b79b306523beed92: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\xa30000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/b89c469e58812fbc: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x92000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/c365544cf81d8e83: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\xa60000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/c36baa3576990ca1: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\b\t0000\xf6\x04000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/c5d82dafaa66f3b7: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\a\b0000\x94000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/d4429f5abb9984ec: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x02\b0000\x950") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/e03eb38bb86be03c: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x05\t0000\x840000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/e4faf5e7df68d2f1: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\n\b0000\x95\x0200000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/ec85fcf226dbe44e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x03\b0000\x9100") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/f15437eae190851e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x03\b0000\x9400") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/f244ff2f55d1103f: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x00\x040000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/writer.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter" 7 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage" 8 | ) 9 | 10 | // Writer is a message writer. 11 | type Writer struct { 12 | w *rawmessage.Writer 13 | } 14 | 15 | // NewWriter allocates a Writer. 16 | func NewWriter( 17 | w io.Writer, 18 | bcw *bytecounter.Writer, 19 | checkAcknowledge bool, 20 | ) *Writer { 21 | return &Writer{ 22 | w: rawmessage.NewWriter(w, bcw, checkAcknowledge), 23 | } 24 | } 25 | 26 | // SetAcknowledgeValue sets the value of the last received acknowledge. 27 | func (w *Writer) SetAcknowledgeValue(v uint32) { 28 | w.w.SetAcknowledgeValue(v) 29 | } 30 | 31 | // Write writes a message. 32 | func (w *Writer) Write(msg Message) error { 33 | raw, err := msg.marshal() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | err = w.w.Write(raw) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | switch tmsg := msg.(type) { 44 | case *SetChunkSize: 45 | w.w.SetChunkSize(tmsg.Value) 46 | 47 | case *SetWindowAckSize: 48 | w.w.SetWindowAckSize(tmsg.Value) 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/message/writer_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter" 10 | ) 11 | 12 | func TestWriter(t *testing.T) { 13 | for _, ca := range readWriterCases { 14 | t.Run(ca.name, func(t *testing.T) { 15 | var buf bytes.Buffer 16 | bc := bytecounter.NewWriter(&buf) 17 | r := NewWriter(bc, bc, true) 18 | err := r.Write(ca.dec) 19 | require.NoError(t, err) 20 | require.Equal(t, ca.enc, buf.Bytes()) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rawmessage/message.go: -------------------------------------------------------------------------------- 1 | // Package rawmessage contains a RTMP raw message reader/writer. 2 | package rawmessage 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | // Message is a raw message. 9 | type Message struct { 10 | ChunkStreamID byte 11 | Timestamp time.Duration 12 | Type uint8 13 | MessageStreamID uint32 14 | Body []byte 15 | } 16 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/19981bffc2abbaf1: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("A") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/2a3abe67115a80dc: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\xbe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/321edca93ba341df: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("0000\x00\x00\x0000000p000\xe200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/7f07c167964a9467: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\xd6") 3 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/rc4_readwriter.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "crypto/rc4" 5 | "io" 6 | ) 7 | 8 | type rc4ReadWriter struct { 9 | rw io.ReadWriter 10 | in *rc4.Cipher 11 | out *rc4.Cipher 12 | } 13 | 14 | func newRC4ReadWriter(rw io.ReadWriter, keyIn []byte, keyOut []byte) (*rc4ReadWriter, error) { 15 | in, err := rc4.NewCipher(keyIn) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | out, err := rc4.NewCipher(keyOut) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | p := make([]byte, 1536) 26 | in.XORKeyStream(p, p) 27 | out.XORKeyStream(p, p) 28 | 29 | return &rc4ReadWriter{ 30 | rw: rw, 31 | in: in, 32 | out: out, 33 | }, nil 34 | } 35 | 36 | func (r *rc4ReadWriter) Read(p []byte) (int, error) { 37 | n, err := r.rw.Read(p) 38 | if n == 0 { 39 | return 0, err 40 | } 41 | 42 | r.in.XORKeyStream(p[:n], p[:n]) 43 | return n, err 44 | } 45 | 46 | func (r *rc4ReadWriter) Write(p []byte) (int, error) { 47 | r.out.XORKeyStream(p, p) 48 | return r.rw.Write(p) 49 | } 50 | -------------------------------------------------------------------------------- /internal/protocols/rtmp/to_stream_test.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestToStreamNoSupportedCodecs(t *testing.T) { 10 | r := &Reader{} 11 | 12 | _, err := ToStream(r, nil) 13 | require.Equal(t, errNoSupportedCodecsTo, err) 14 | } 15 | 16 | // this is impossible to test since currently we support all RTMP tracks. 17 | // func TestToStreamSkipUnsupportedTracks(t *testing.T) 18 | -------------------------------------------------------------------------------- /internal/protocols/rtsp/credentials.go: -------------------------------------------------------------------------------- 1 | package rtsp 2 | 3 | import ( 4 | "github.com/bluenviron/gortsplib/v4/pkg/base" 5 | "github.com/bluenviron/gortsplib/v4/pkg/headers" 6 | "github.com/bluenviron/mediamtx/internal/auth" 7 | ) 8 | 9 | // Credentials extracts credentials from a RTSP request. 10 | func Credentials(rt *base.Request) *auth.Credentials { 11 | c := &auth.Credentials{} 12 | 13 | var rtspAuthHeader headers.Authorization 14 | err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"]) 15 | if err == nil { 16 | c.User = rtspAuthHeader.Username 17 | if rtspAuthHeader.Method == headers.AuthMethodBasic { 18 | c.Pass = rtspAuthHeader.BasicPass 19 | } 20 | } 21 | 22 | return c 23 | } 24 | -------------------------------------------------------------------------------- /internal/protocols/rtsp/credentials_test.go: -------------------------------------------------------------------------------- 1 | package rtsp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bluenviron/gortsplib/v4/pkg/base" 7 | "github.com/bluenviron/mediamtx/internal/auth" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCredentials(t *testing.T) { 12 | rr := &base.Request{ 13 | Header: base.Header{ 14 | "Authorization": []string{ 15 | "Basic bXl1c2VyOm15cGFzcw==", 16 | }, 17 | }, 18 | } 19 | 20 | c := Credentials(rr) 21 | 22 | require.Equal(t, &auth.Credentials{ 23 | User: "myuser", 24 | Pass: "mypass", 25 | }, c) 26 | } 27 | -------------------------------------------------------------------------------- /internal/protocols/tls/tls_config.go: -------------------------------------------------------------------------------- 1 | // Package tls contains TLS utilities. 2 | package tls 3 | 4 | import ( 5 | "crypto/sha256" 6 | "crypto/tls" 7 | "encoding/hex" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // ConfigForFingerprint returns a tls.Config that supports given fingerprint. 13 | func ConfigForFingerprint(fingerprint string) *tls.Config { 14 | if fingerprint == "" { 15 | return nil 16 | } 17 | 18 | fingerprintLower := strings.ToLower(fingerprint) 19 | 20 | return &tls.Config{ 21 | InsecureSkipVerify: true, 22 | VerifyConnection: func(cs tls.ConnectionState) error { 23 | h := sha256.New() 24 | h.Write(cs.PeerCertificates[0].Raw) 25 | hstr := hex.EncodeToString(h.Sum(nil)) 26 | 27 | if hstr != fingerprintLower { 28 | return fmt.Errorf("source fingerprint does not match: expected %s, got %s", 29 | fingerprintLower, hstr) 30 | } 31 | 32 | return nil 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/protocols/websocket/serverconn_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestServerConn(t *testing.T) { 15 | pingReceived := make(chan struct{}) 16 | pingInterval = 100 * time.Millisecond 17 | 18 | s := &http.Server{ 19 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | c, err := NewServerConn(w, r) 21 | require.NoError(t, err) 22 | defer c.Close() 23 | 24 | err = c.WriteJSON("testing") 25 | require.NoError(t, err) 26 | 27 | <-pingReceived 28 | }), 29 | } 30 | 31 | ln, err := net.Listen("tcp", "localhost:6344") 32 | require.NoError(t, err) 33 | 34 | go s.Serve(ln) 35 | defer s.Shutdown(context.Background()) 36 | 37 | c, res, err := websocket.DefaultDialer.Dial("ws://localhost:6344/", nil) 38 | require.NoError(t, err) 39 | defer res.Body.Close() 40 | defer c.Close() //nolint:errcheck 41 | 42 | c.SetPingHandler(func(_ string) error { 43 | close(pingReceived) 44 | return nil 45 | }) 46 | 47 | var msg string 48 | err = c.ReadJSON(&msg) 49 | require.NoError(t, err) 50 | require.Equal(t, "testing", msg) 51 | 52 | _, _, err = c.ReadMessage() 53 | require.Error(t, err) 54 | 55 | <-pingReceived 56 | } 57 | -------------------------------------------------------------------------------- /internal/protocols/whip/link_header.go: -------------------------------------------------------------------------------- 1 | package whip 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/pion/webrtc/v4" 9 | ) 10 | 11 | func quoteCredential(v string) string { 12 | b, _ := json.Marshal(v) 13 | s := string(b) 14 | return s[1 : len(s)-1] 15 | } 16 | 17 | func unquoteCredential(v string) string { 18 | var s string 19 | json.Unmarshal([]byte("\""+v+"\""), &s) //nolint:errcheck 20 | return s 21 | } 22 | 23 | // LinkHeaderMarshal encodes a link header. 24 | func LinkHeaderMarshal(iceServers []webrtc.ICEServer) []string { 25 | ret := make([]string, len(iceServers)) 26 | 27 | for i, server := range iceServers { 28 | link := "<" + server.URLs[0] + ">; rel=\"ice-server\"" 29 | if server.Username != "" { 30 | link += "; username=\"" + quoteCredential(server.Username) + "\"" + 31 | "; credential=\"" + quoteCredential(server.Credential.(string)) + "\"; credential-type=\"password\"" 32 | } 33 | ret[i] = link 34 | } 35 | 36 | return ret 37 | } 38 | 39 | var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` + 40 | `; credential="(.+?)"; credential-type="password")?`) 41 | 42 | // LinkHeaderUnmarshal decodes a link header. 43 | func LinkHeaderUnmarshal(link []string) ([]webrtc.ICEServer, error) { 44 | ret := make([]webrtc.ICEServer, len(link)) 45 | 46 | for i, li := range link { 47 | m := reLink.FindStringSubmatch(li) 48 | if m == nil { 49 | return nil, fmt.Errorf("invalid link header: '%s'", li) 50 | } 51 | 52 | s := webrtc.ICEServer{ 53 | URLs: []string{m[1]}, 54 | } 55 | 56 | if m[3] != "" { 57 | s.Username = unquoteCredential(m[3]) 58 | s.Credential = unquoteCredential(m[4]) 59 | s.CredentialType = webrtc.ICECredentialTypePassword 60 | } 61 | 62 | ret[i] = s 63 | } 64 | 65 | return ret, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/protocols/whip/link_header_test.go: -------------------------------------------------------------------------------- 1 | package whip 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pion/webrtc/v4" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var linkHeaderCases = []struct { 11 | name string 12 | enc []string 13 | dec []webrtc.ICEServer 14 | }{ 15 | { 16 | "a", 17 | []string{ 18 | `; rel="ice-server"`, 19 | `; rel="ice-server"; username="myuser\"a?2;B"; ` + 20 | `credential="mypwd"; credential-type="password"`, 21 | }, 22 | []webrtc.ICEServer{ 23 | { 24 | URLs: []string{"stun:stun.l.google.com:19302"}, 25 | }, 26 | { 27 | URLs: []string{"turns:turn.example.com"}, 28 | Username: "myuser\"a?2;B", 29 | Credential: "mypwd", 30 | }, 31 | }, 32 | }, 33 | } 34 | 35 | func TestLinkHeaderUnmarshal(t *testing.T) { 36 | for _, ca := range linkHeaderCases { 37 | t.Run(ca.name, func(t *testing.T) { 38 | dec, err := LinkHeaderUnmarshal(ca.enc) 39 | require.NoError(t, err) 40 | require.Equal(t, ca.dec, dec) 41 | }) 42 | } 43 | } 44 | 45 | func TestLinkHeaderMarshal(t *testing.T) { 46 | for _, ca := range linkHeaderCases { 47 | t.Run(ca.name, func(t *testing.T) { 48 | enc := LinkHeaderMarshal(ca.dec) 49 | require.Equal(t, ca.enc, enc) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/recorder/format.go: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | type format interface { 4 | initialize() bool 5 | close() 6 | } 7 | -------------------------------------------------------------------------------- /internal/recorder/format_mpegts_segment.go: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/bluenviron/mediamtx/internal/logger" 9 | "github.com/bluenviron/mediamtx/internal/recordstore" 10 | ) 11 | 12 | type formatMPEGTSSegment struct { 13 | f *formatMPEGTS 14 | startDTS time.Duration 15 | startNTP time.Time 16 | 17 | path string 18 | fi *os.File 19 | lastFlush time.Duration 20 | lastDTS time.Duration 21 | } 22 | 23 | func (s *formatMPEGTSSegment) initialize() { 24 | s.lastFlush = s.startDTS 25 | s.lastDTS = s.startDTS 26 | s.f.dw.setTarget(s) 27 | } 28 | 29 | func (s *formatMPEGTSSegment) close() error { 30 | err := s.f.bw.Flush() 31 | 32 | if s.fi != nil { 33 | s.f.ri.Log(logger.Debug, "closing segment %s", s.path) 34 | err2 := s.fi.Close() 35 | if err == nil { 36 | err = err2 37 | } 38 | 39 | if err2 == nil { 40 | duration := s.lastDTS - s.startDTS 41 | s.f.ri.onSegmentComplete(s.path, duration) 42 | } 43 | } 44 | 45 | return err 46 | } 47 | 48 | func (s *formatMPEGTSSegment) Write(p []byte) (int, error) { 49 | if s.fi == nil { 50 | s.path = recordstore.Path{Start: s.startNTP}.Encode(s.f.ri.pathFormat2) 51 | s.f.ri.Log(logger.Debug, "creating segment %s", s.path) 52 | 53 | err := os.MkdirAll(filepath.Dir(s.path), 0o755) 54 | if err != nil { 55 | return 0, err 56 | } 57 | 58 | fi, err := os.Create(s.path) 59 | if err != nil { 60 | return 0, err 61 | } 62 | 63 | s.f.ri.onSegmentCreate(s.path) 64 | 65 | s.fi = fi 66 | } 67 | 68 | return s.fi.Write(p) 69 | } 70 | -------------------------------------------------------------------------------- /internal/recordstore/recordstore.go: -------------------------------------------------------------------------------- 1 | // Package recordstore contains utilities to store/retrieve recordings to/from disk. 2 | package recordstore 3 | -------------------------------------------------------------------------------- /internal/restrictnetwork/restrict_network.go: -------------------------------------------------------------------------------- 1 | // Package restrictnetwork contains Restrict(). 2 | package restrictnetwork 3 | 4 | import ( 5 | "net" 6 | ) 7 | 8 | // Restrict prevents listening on IPv6 when address is 0.0.0.0. 9 | func Restrict(network string, address string) (string, string) { 10 | host, _, err := net.SplitHostPort(address) 11 | if err == nil { 12 | if host == "0.0.0.0" { 13 | return network + "4", address 14 | } 15 | } 16 | 17 | return network, address 18 | } 19 | -------------------------------------------------------------------------------- /internal/rlimit/rlimit_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | // Package rlimit contains a function to raise rlimit. 4 | package rlimit 5 | 6 | import ( 7 | "syscall" 8 | ) 9 | 10 | // Raise raises the number of file descriptors that can be opened. 11 | func Raise() error { 12 | var rlim syscall.Rlimit 13 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | rlim.Cur = 999999 19 | err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rlimit/rlimit_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package rlimit 4 | 5 | func Raise() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /internal/servers/hls/hlsjsdownloader/HASH: -------------------------------------------------------------------------------- 1 | 3910efa9d260b2a2785e38e4c126b3a850aed53f21a88780b64f5d9f3a65773c 2 | -------------------------------------------------------------------------------- /internal/servers/hls/hlsjsdownloader/VERSION: -------------------------------------------------------------------------------- 1 | v1.6.5 2 | -------------------------------------------------------------------------------- /internal/servers/hls/hlsjsdownloader/main.go: -------------------------------------------------------------------------------- 1 | // Package main contains an utility to download hls.js 2 | package main 3 | 4 | import ( 5 | "archive/zip" 6 | "bytes" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "fmt" 10 | "io" 11 | "io/fs" 12 | "log" 13 | "net/http" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | func do() error { 19 | buf, err := os.ReadFile("./hlsjsdownloader/VERSION") 20 | if err != nil { 21 | return err 22 | } 23 | version := strings.TrimSpace(string(buf)) 24 | 25 | log.Printf("downloading hls.js %s...", version) 26 | 27 | res, err := http.Get("https://github.com/video-dev/hls.js/releases/download/" + version + "/release.zip") 28 | if err != nil { 29 | return err 30 | } 31 | defer res.Body.Close() 32 | 33 | if res.StatusCode != http.StatusOK { 34 | return fmt.Errorf("bad status code: %v", res.StatusCode) 35 | } 36 | 37 | zipBuf, err := io.ReadAll(res.Body) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | buf, err = os.ReadFile("./hlsjsdownloader/HASH") 43 | if err != nil { 44 | return err 45 | } 46 | str := strings.TrimSpace(string(buf)) 47 | 48 | hash, err := hex.DecodeString(str) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if sum := sha256.Sum256(zipBuf); !bytes.Equal(sum[:], hash) { 54 | return fmt.Errorf("hash mismatch") 55 | } 56 | 57 | z, err := zip.NewReader(bytes.NewReader(zipBuf), int64(len(zipBuf))) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | hls, err := fs.ReadFile(z, "dist/hls.min.js") 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if err = os.WriteFile("hls.min.js", hls, 0o644); err != nil { 68 | return err 69 | } 70 | 71 | log.Println("ok") 72 | return nil 73 | } 74 | 75 | func main() { 76 | err := do() 77 | if err != nil { 78 | log.Printf("ERR: %v", err) 79 | os.Exit(1) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/servers/rtmp/listener.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | ) 7 | 8 | type listener struct { 9 | ln net.Listener 10 | wg *sync.WaitGroup 11 | parent *Server 12 | } 13 | 14 | func (l *listener) initialize() { 15 | l.wg.Add(1) 16 | go l.run() 17 | } 18 | 19 | func (l *listener) run() { 20 | defer l.wg.Done() 21 | 22 | err := l.runInner() 23 | 24 | l.parent.acceptError(err) 25 | } 26 | 27 | func (l *listener) runInner() error { 28 | for { 29 | conn, err := l.ln.Accept() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | l.parent.newConn(conn) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/servers/srt/listener.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "sync" 5 | 6 | srt "github.com/datarhei/gosrt" 7 | ) 8 | 9 | type listener struct { 10 | ln srt.Listener 11 | wg *sync.WaitGroup 12 | parent *Server 13 | } 14 | 15 | func (l *listener) initialize() { 16 | l.wg.Add(1) 17 | go l.run() 18 | } 19 | 20 | func (l *listener) run() { 21 | defer l.wg.Done() 22 | 23 | err := l.runInner() 24 | 25 | l.parent.acceptError(err) 26 | } 27 | 28 | func (l *listener) runInner() error { 29 | for { 30 | req, err := l.ln.Accept2() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | l.parent.newConnRequest(req) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/servers/srt/streamid_test.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestStreamIDUnmarshal(t *testing.T) { 10 | for _, ca := range []struct { 11 | name string 12 | raw string 13 | dec streamID 14 | }{ 15 | { 16 | "mediamtx syntax 1", 17 | "read:mypath", 18 | streamID{ 19 | mode: streamIDModeRead, 20 | path: "mypath", 21 | }, 22 | }, 23 | { 24 | "mediamtx syntax 2", 25 | "publish:mypath:myquery", 26 | streamID{ 27 | mode: streamIDModePublish, 28 | path: "mypath", 29 | query: "myquery", 30 | }, 31 | }, 32 | { 33 | "mediamtx syntax 3", 34 | "read:mypath:myuser:mypass:myquery", 35 | streamID{ 36 | mode: streamIDModeRead, 37 | path: "mypath", 38 | user: "myuser", 39 | pass: "mypass", 40 | query: "myquery", 41 | }, 42 | }, 43 | { 44 | "standard syntax", 45 | "#!::u=johnny,t=file,m=publish,r=results.csv,s=mypass,h=myhost.com", 46 | streamID{ 47 | mode: streamIDModePublish, 48 | path: "results.csv", 49 | user: "johnny", 50 | pass: "mypass", 51 | }, 52 | }, 53 | { 54 | "issue 3701", 55 | "#!::bmd_uuid=0e1df79f-77e6-465c-b099-29a616e964f7,bmd_name=rdt-wp-003,r=test3,m=publish", 56 | streamID{ 57 | mode: streamIDModePublish, 58 | path: "test3", 59 | }, 60 | }, 61 | } { 62 | t.Run(ca.name, func(t *testing.T) { 63 | var sid streamID 64 | err := sid.unmarshal(ca.raw) 65 | require.NoError(t, err) 66 | require.Equal(t, ca.dec, sid) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/camera_32.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm 2 | 3 | package rpicamera 4 | 5 | import ( 6 | "embed" 7 | ) 8 | 9 | //go:embed mtxrpicam_32/* 10 | var mtxrpicam embed.FS 11 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/camera_64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && arm64 2 | 3 | package rpicamera 4 | 5 | import ( 6 | "embed" 7 | ) 8 | 9 | //go:embed mtxrpicam_64/* 10 | var mtxrpicam embed.FS 11 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/camera_disabled.go: -------------------------------------------------------------------------------- 1 | //go:build !linux || (!arm && !arm64) 2 | 3 | package rpicamera 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | type camera struct { 11 | params params 12 | onData func(int64, time.Time, [][]byte) 13 | onDataSecondary func(int64, time.Time, []byte) 14 | } 15 | 16 | func (c *camera) initialize() error { 17 | return fmt.Errorf("server was compiled without support for the Raspberry Pi Camera") 18 | } 19 | 20 | func (c *camera) close() { 21 | } 22 | 23 | func (c *camera) reloadParams(_ params) { 24 | } 25 | 26 | func (c *camera) wait() error { 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/camera_download.go: -------------------------------------------------------------------------------- 1 | package rpicamera 2 | 3 | //go:generate go run ./mtxrpicamdownloader 4 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ: -------------------------------------------------------------------------------- 1 | c5f4db9d0fd05bfb8e692c1f33d06d9f06b93084706bcf384ee2a823757f4299 2 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ: -------------------------------------------------------------------------------- 1 | f3499c34dc6190f158098fa457893d76b410b0e99386b19dd3888e9ed12bab25 2 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION: -------------------------------------------------------------------------------- 1 | v2.4.2 2 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/params.go: -------------------------------------------------------------------------------- 1 | package rpicamera 2 | 3 | type params struct { 4 | LogLevel string 5 | CameraID uint32 6 | Width uint32 7 | Height uint32 8 | HFlip bool 9 | VFlip bool 10 | Brightness float32 11 | Contrast float32 12 | Saturation float32 13 | Sharpness float32 14 | Exposure string 15 | AWB string 16 | AWBGainRed float32 17 | AWBGainBlue float32 18 | Denoise string 19 | Shutter uint32 20 | Metering string 21 | Gain float32 22 | EV float32 23 | ROI string 24 | HDR bool 25 | TuningFile string 26 | Mode string 27 | FPS float32 28 | AfMode string 29 | AfRange string 30 | AfSpeed string 31 | LensPosition float32 32 | AfWindow string 33 | FlickerPeriod uint32 34 | TextOverlayEnable bool 35 | TextOverlay string 36 | Codec string 37 | IDRPeriod uint32 38 | Bitrate uint32 39 | Profile string 40 | Level string 41 | SecondaryWidth uint32 42 | SecondaryHeight uint32 43 | SecondaryFPS float32 44 | SecondaryQuality uint32 45 | } 46 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/params_serialize.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && arm) || (linux && arm64) 2 | 3 | package rpicamera 4 | 5 | import ( 6 | "encoding/base64" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func (p params) serialize() []byte { 13 | rv := reflect.ValueOf(p) 14 | rt := rv.Type() 15 | nf := rv.NumField() 16 | ret := make([]string, nf) 17 | 18 | for i := range nf { 19 | entry := rt.Field(i).Name + ":" 20 | f := rv.Field(i) 21 | v := f.Interface() 22 | 23 | switch v := v.(type) { 24 | case uint32: 25 | entry += strconv.FormatUint(uint64(v), 10) 26 | 27 | case float32: 28 | entry += strconv.FormatFloat(float64(v), 'f', -1, 64) 29 | 30 | case string: 31 | entry += base64.StdEncoding.EncodeToString([]byte(v)) 32 | 33 | case bool: 34 | if f.Bool() { 35 | entry += "1" 36 | } else { 37 | entry += "0" 38 | } 39 | 40 | default: 41 | panic("unhandled type") 42 | } 43 | 44 | ret[i] = entry 45 | } 46 | 47 | return []byte(strings.Join(ret, " ")) 48 | } 49 | -------------------------------------------------------------------------------- /internal/staticsources/rpicamera/pipe.go: -------------------------------------------------------------------------------- 1 | //go:build (linux && arm) || (linux && arm64) 2 | 3 | package rpicamera 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func syscallReadAll(fd int, buf []byte) error { 10 | size := len(buf) 11 | read := 0 12 | 13 | for { 14 | n, err := syscall.Read(fd, buf[read:size]) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | read += n 20 | if read >= size { 21 | break 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | type pipe struct { 29 | readFD int 30 | writeFD int 31 | } 32 | 33 | func newPipe() (*pipe, error) { 34 | fds := make([]int, 2) 35 | err := syscall.Pipe(fds) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &pipe{ 41 | readFD: fds[0], 42 | writeFD: fds[1], 43 | }, nil 44 | } 45 | 46 | func (p *pipe) close() { 47 | syscall.Close(p.readFD) 48 | syscall.Close(p.writeFD) 49 | } 50 | 51 | func (p *pipe) read() ([]byte, error) { 52 | buf := make([]byte, 4) 53 | err := syscallReadAll(p.readFD, buf) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | le := int(buf[3])<<24 | int(buf[2])<<16 | int(buf[1])<<8 | int(buf[0]) 59 | buf = make([]byte, le) 60 | 61 | err = syscallReadAll(p.readFD, buf) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return buf, nil 67 | } 68 | 69 | func (p *pipe) write(byts []byte) error { 70 | le := len(byts) 71 | _, err := syscall.Write(p.writeFD, []byte{byte(le), byte(le >> 8), byte(le >> 16), byte(le >> 24)}) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | _, err = syscall.Write(p.writeFD, byts) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /internal/staticsources/srt/source_test.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "bufio" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts" 9 | srt "github.com/datarhei/gosrt" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/bluenviron/mediamtx/internal/conf" 13 | "github.com/bluenviron/mediamtx/internal/defs" 14 | "github.com/bluenviron/mediamtx/internal/test" 15 | ) 16 | 17 | func TestSource(t *testing.T) { 18 | ln, err := srt.Listen("srt", "127.0.0.1:9002", srt.DefaultConfig()) 19 | require.NoError(t, err) 20 | defer ln.Close() 21 | 22 | go func() { 23 | req, err := ln.Accept2() 24 | require.NoError(t, err) 25 | 26 | require.Equal(t, "sidname", req.StreamId()) 27 | err = req.SetPassphrase("ttest1234567") 28 | require.NoError(t, err) 29 | 30 | conn, err := req.Accept() 31 | require.NoError(t, err) 32 | defer conn.Close() 33 | 34 | track := &mpegts.Track{ 35 | Codec: &mpegts.CodecH264{}, 36 | } 37 | 38 | bw := bufio.NewWriter(conn) 39 | w := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}} 40 | err = w.Initialize() 41 | require.NoError(t, err) 42 | 43 | err = w.WriteH264(track, 0, 0, [][]byte{{ // IDR 44 | 5, 1, 45 | }}) 46 | require.NoError(t, err) 47 | 48 | err = bw.Flush() 49 | require.NoError(t, err) 50 | 51 | // wait for internal SRT queue to be written 52 | time.Sleep(500 * time.Millisecond) 53 | }() 54 | 55 | te := test.NewSourceTester( 56 | func(p defs.StaticSourceParent) defs.StaticSource { 57 | return &Source{ 58 | ReadTimeout: conf.Duration(10 * time.Second), 59 | Parent: p, 60 | } 61 | }, 62 | "srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567", 63 | &conf.Path{}, 64 | ) 65 | defer te.Close() 66 | 67 | <-te.Unit 68 | } 69 | -------------------------------------------------------------------------------- /internal/stream/stream_media.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "github.com/bluenviron/gortsplib/v4/pkg/description" 5 | "github.com/bluenviron/gortsplib/v4/pkg/format" 6 | "github.com/bluenviron/mediamtx/internal/counterdumper" 7 | "github.com/bluenviron/mediamtx/internal/logger" 8 | ) 9 | 10 | type streamMedia struct { 11 | udpMaxPayloadSize int 12 | media *description.Media 13 | generateRTPPackets bool 14 | processingErrors *counterdumper.CounterDumper 15 | parent logger.Writer 16 | 17 | formats map[format.Format]*streamFormat 18 | } 19 | 20 | func (sm *streamMedia) initialize() error { 21 | sm.formats = make(map[format.Format]*streamFormat) 22 | 23 | for _, forma := range sm.media.Formats { 24 | sf := &streamFormat{ 25 | udpMaxPayloadSize: sm.udpMaxPayloadSize, 26 | format: forma, 27 | generateRTPPackets: sm.generateRTPPackets, 28 | processingErrors: sm.processingErrors, 29 | parent: sm.parent, 30 | } 31 | err := sf.initialize() 32 | if err != nil { 33 | return err 34 | } 35 | sm.formats[forma] = sf 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/stream/stream_reader.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bluenviron/gortsplib/v4/pkg/ringbuffer" 7 | "github.com/bluenviron/mediamtx/internal/counterdumper" 8 | "github.com/bluenviron/mediamtx/internal/logger" 9 | ) 10 | 11 | type streamReader struct { 12 | queueSize int 13 | parent logger.Writer 14 | 15 | buffer *ringbuffer.RingBuffer 16 | started bool 17 | discardedFrames *counterdumper.CounterDumper 18 | 19 | // out 20 | err chan error 21 | } 22 | 23 | func (w *streamReader) initialize() { 24 | buffer, _ := ringbuffer.New(uint64(w.queueSize)) 25 | w.buffer = buffer 26 | w.err = make(chan error) 27 | } 28 | 29 | func (w *streamReader) start() { 30 | w.started = true 31 | 32 | w.discardedFrames = &counterdumper.CounterDumper{ 33 | OnReport: func(val uint64) { 34 | w.parent.Log(logger.Warn, "connection is too slow, discarding %d %s", 35 | val, 36 | func() string { 37 | if val == 1 { 38 | return "frame" 39 | } 40 | return "frames" 41 | }()) 42 | }, 43 | } 44 | w.discardedFrames.Start() 45 | 46 | go w.run() 47 | } 48 | 49 | func (w *streamReader) stop() { 50 | w.buffer.Close() 51 | 52 | if w.started { 53 | w.discardedFrames.Stop() 54 | <-w.err 55 | } 56 | } 57 | 58 | func (w *streamReader) error() chan error { 59 | return w.err 60 | } 61 | 62 | func (w *streamReader) run() { 63 | w.err <- w.runInner() 64 | close(w.err) 65 | } 66 | 67 | func (w *streamReader) runInner() error { 68 | for { 69 | cb, ok := w.buffer.Pull() 70 | if !ok { 71 | return fmt.Errorf("terminated") 72 | } 73 | 74 | err := cb.(func() error)() 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | } 80 | 81 | func (w *streamReader) push(cb func() error) { 82 | ok := w.buffer.Push(cb) 83 | if !ok { 84 | w.discardedFrames.Increase() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/test/auth_manager.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "github.com/bluenviron/mediamtx/internal/auth" 4 | 5 | // AuthManager is a dummy auth manager. 6 | type AuthManager struct { 7 | AuthenticateImpl func(req *auth.Request) error 8 | RefreshJWTJWKSImpl func() 9 | } 10 | 11 | // Authenticate replicates auth.Manager.Replicate 12 | func (m *AuthManager) Authenticate(req *auth.Request) error { 13 | return m.AuthenticateImpl(req) 14 | } 15 | 16 | // RefreshJWTJWKS is a function that simulates a JWKS refresh. 17 | func (m *AuthManager) RefreshJWTJWKS() { 18 | m.RefreshJWTJWKSImpl() 19 | } 20 | 21 | // NilAuthManager is an auth manager that accepts everything. 22 | var NilAuthManager = &AuthManager{ 23 | AuthenticateImpl: func(_ *auth.Request) error { 24 | return nil 25 | }, 26 | RefreshJWTJWKSImpl: func() { 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /internal/test/formats.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/bluenviron/gortsplib/v4/pkg/format" 5 | "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" 6 | ) 7 | 8 | // FormatH264 is a dummy H264 format. 9 | var FormatH264 = &format.H264{ 10 | PayloadTyp: 96, 11 | SPS: []byte{ // 1920x1080 baseline 12 | 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, 13 | 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, 14 | 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, 15 | }, 16 | PPS: []byte{0x08, 0x06, 0x07, 0x08}, 17 | PacketizationMode: 1, 18 | } 19 | 20 | // FormatH265 is a dummy H265 format. 21 | var FormatH265 = &format.H265{ 22 | PayloadTyp: 96, 23 | VPS: []byte{ 24 | 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, 25 | 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, 26 | 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24, 27 | }, 28 | SPS: []byte{ 29 | 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 30 | 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 31 | 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, 32 | 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, 33 | 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, 34 | 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, 35 | 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, 36 | 0x02, 0x02, 0x02, 0x01, 37 | }, 38 | PPS: []byte{ 39 | 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40, 40 | }, 41 | } 42 | 43 | // FormatMPEG4Audio is a dummy MPEG-4 audio format. 44 | var FormatMPEG4Audio = &format.MPEG4Audio{ 45 | PayloadTyp: 96, 46 | Config: &mpeg4audio.Config{ 47 | Type: 2, 48 | SampleRate: 44100, 49 | ChannelCount: 2, 50 | }, 51 | SizeLength: 13, 52 | IndexLength: 3, 53 | IndexDeltaLength: 3, 54 | } 55 | -------------------------------------------------------------------------------- /internal/test/logger.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "github.com/bluenviron/mediamtx/internal/logger" 4 | 5 | type nilLogger struct{} 6 | 7 | func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) { 8 | } 9 | 10 | // NilLogger is a logger to /dev/null 11 | var NilLogger logger.Writer = &nilLogger{} 12 | 13 | type testLogger struct { 14 | cb func(level logger.Level, format string, args ...interface{}) 15 | } 16 | 17 | func (l *testLogger) Log(level logger.Level, format string, args ...interface{}) { 18 | l.cb(level, format, args...) 19 | } 20 | 21 | // Logger returns a dummy logger. 22 | func Logger(cb func(logger.Level, string, ...interface{})) logger.Writer { 23 | return &testLogger{cb: cb} 24 | } 25 | -------------------------------------------------------------------------------- /internal/test/medias.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/bluenviron/gortsplib/v4/pkg/description" 5 | "github.com/bluenviron/gortsplib/v4/pkg/format" 6 | ) 7 | 8 | // MediaH264 is a dummy H264 media. 9 | var MediaH264 = UniqueMediaH264() 10 | 11 | // MediaMPEG4Audio is a dummy MPEG-4 audio media. 12 | var MediaMPEG4Audio = UniqueMediaMPEG4Audio() 13 | 14 | // UniqueMediaH264 is a dummy H264 media. 15 | func UniqueMediaH264() *description.Media { 16 | return &description.Media{ 17 | Type: description.MediaTypeVideo, 18 | Formats: []format.Format{FormatH264}, 19 | } 20 | } 21 | 22 | // UniqueMediaMPEG4Audio is a dummy MPEG-4 audio media. 23 | func UniqueMediaMPEG4Audio() *description.Media { 24 | return &description.Media{ 25 | Type: description.MediaTypeAudio, 26 | Formats: []format.Format{FormatMPEG4Audio}, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/test/path_manager.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/bluenviron/mediamtx/internal/conf" 5 | "github.com/bluenviron/mediamtx/internal/defs" 6 | "github.com/bluenviron/mediamtx/internal/stream" 7 | ) 8 | 9 | // PathManager is a dummy path manager. 10 | type PathManager struct { 11 | FindPathConfImpl func(req defs.PathFindPathConfReq) (*conf.Path, error) 12 | DescribeImpl func(req defs.PathDescribeReq) defs.PathDescribeRes 13 | AddPublisherImpl func(req defs.PathAddPublisherReq) (defs.Path, error) 14 | AddReaderImpl func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) 15 | } 16 | 17 | // FindPathConf implements PathManager. 18 | func (pm *PathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) { 19 | return pm.FindPathConfImpl(req) 20 | } 21 | 22 | // Describe implements PathManager. 23 | func (pm *PathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes { 24 | return pm.DescribeImpl(req) 25 | } 26 | 27 | // AddPublisher implements PathManager. 28 | func (pm *PathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) { 29 | return pm.AddPublisherImpl(req) 30 | } 31 | 32 | // AddReader implements PathManager. 33 | func (pm *PathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { 34 | return pm.AddReaderImpl(req) 35 | } 36 | -------------------------------------------------------------------------------- /internal/test/temp_file.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "os" 4 | 5 | // CreateTempFile creates a temporary file with given content. 6 | func CreateTempFile(byts []byte) (string, error) { 7 | tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-") 8 | if err != nil { 9 | return "", err 10 | } 11 | defer tmpf.Close() 12 | 13 | _, err = tmpf.Write(byts) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | return tmpf.Name(), nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/teste2e/build_images_test.go: -------------------------------------------------------------------------------- 1 | //go:build enable_e2e_tests 2 | 3 | package teste2e 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func buildImage(image string) error { 15 | ecmd := exec.Command("docker", "build", filepath.Join("images", image), 16 | "-t", "mediamtx-test-"+image) 17 | ecmd.Stdout = nil 18 | ecmd.Stderr = os.Stderr 19 | return ecmd.Run() 20 | } 21 | 22 | func TestBuildImages(t *testing.T) { 23 | files, err := os.ReadDir("images") 24 | require.NoError(t, err) 25 | 26 | for _, file := range files { 27 | err := buildImage(file.Name()) 28 | require.NoError(t, err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/teste2e/images/ffmpeg/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amd64/alpine:3.14 2 | 3 | RUN apk add --no-cache \ 4 | ffmpeg 5 | 6 | COPY *.mkv / 7 | 8 | COPY start.sh / 9 | RUN chmod +x /start.sh 10 | 11 | ENTRYPOINT [ "/start.sh" ] 12 | -------------------------------------------------------------------------------- /internal/teste2e/images/ffmpeg/emptyvideo.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluenviron/mediamtx/870b99f69ffcb2f65b30114d803511bd553e83f7/internal/teste2e/images/ffmpeg/emptyvideo.mkv -------------------------------------------------------------------------------- /internal/teste2e/images/ffmpeg/emptyvideoaudio.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluenviron/mediamtx/870b99f69ffcb2f65b30114d803511bd553e83f7/internal/teste2e/images/ffmpeg/emptyvideoaudio.mkv -------------------------------------------------------------------------------- /internal/teste2e/images/ffmpeg/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec ffmpeg -hide_banner -loglevel error $@ 2>&1 4 | -------------------------------------------------------------------------------- /internal/teste2e/images/gstreamer/Dockerfile: -------------------------------------------------------------------------------- 1 | ###################################### 2 | FROM ubuntu:20.04 AS build 3 | 4 | RUN apt update && apt install -y --no-install-recommends \ 5 | pkg-config \ 6 | gcc \ 7 | libgstreamer-plugins-base1.0-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | COPY exitafterframe.c /s/ 11 | RUN cd /s \ 12 | && gcc \ 13 | exitafterframe.c \ 14 | -o libexitafterframe.so \ 15 | -Ofast \ 16 | -s \ 17 | -Werror \ 18 | -Wall \ 19 | -Wextra \ 20 | -Wno-unused-parameter \ 21 | -fPIC \ 22 | -shared \ 23 | -Wl,--no-undefined \ 24 | $(pkg-config --cflags --libs gstreamer-1.0) \ 25 | && mv libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ \ 26 | && rm -rf /s 27 | 28 | ###################################### 29 | FROM ubuntu:20.04 30 | 31 | RUN apt update && apt install -y --no-install-recommends \ 32 | gstreamer1.0-tools \ 33 | gstreamer1.0-plugins-good \ 34 | gstreamer1.0-plugins-bad \ 35 | gstreamer1.0-rtsp \ 36 | gstreamer1.0-libav \ 37 | && rm -rf /var/lib/apt/lists/* 38 | 39 | COPY --from=build /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ 40 | 41 | COPY emptyvideo.mkv / 42 | 43 | COPY start.sh / 44 | RUN chmod +x /start.sh 45 | 46 | ENTRYPOINT [ "/start.sh" ] 47 | -------------------------------------------------------------------------------- /internal/teste2e/images/gstreamer/emptyvideo.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluenviron/mediamtx/870b99f69ffcb2f65b30114d803511bd553e83f7/internal/teste2e/images/gstreamer/emptyvideo.mkv -------------------------------------------------------------------------------- /internal/teste2e/images/gstreamer/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec gst-launch-1.0 $@ 2>&1 4 | -------------------------------------------------------------------------------- /internal/teste2e/images/vlc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amd64/alpine:3.14 2 | 3 | RUN apk add --no-cache \ 4 | vlc 5 | 6 | RUN adduser -D -H -s /bin/sh -u 9337 user 7 | 8 | COPY start.sh / 9 | RUN chmod +x /start.sh 10 | 11 | RUN mkdir /out \ 12 | && chown user:user /out 13 | 14 | USER user 15 | ENTRYPOINT [ "/start.sh" ] 16 | -------------------------------------------------------------------------------- /internal/teste2e/images/vlc/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cvlc --play-and-exit --no-audio --no-video --sout file/ts:/out/stream.ts -vvv $@ 2>&1 & 4 | 5 | COUNTER=0 6 | while true; do 7 | sleep 1 8 | 9 | if [ $(stat -c "%s" /out/stream.ts) -gt 0 ]; then 10 | exit 0 11 | fi 12 | 13 | COUNTER=$(($COUNTER + 1)) 14 | 15 | if [ $COUNTER -ge 15 ]; then 16 | exit 1 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /internal/teste2e/tests_test.go: -------------------------------------------------------------------------------- 1 | //go:build enable_e2e_tests 2 | 3 | package teste2e 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/bluenviron/mediamtx/internal/core" 12 | "github.com/bluenviron/mediamtx/internal/test" 13 | ) 14 | 15 | func newInstance(conf string) (*core.Core, bool) { 16 | if conf == "" { 17 | return core.New([]string{}) 18 | } 19 | 20 | tmpf, err := test.CreateTempFile([]byte(conf)) 21 | if err != nil { 22 | return nil, false 23 | } 24 | defer os.Remove(tmpf) 25 | 26 | return core.New([]string{tmpf}) 27 | } 28 | 29 | type container struct { 30 | name string 31 | } 32 | 33 | func newContainer(image string, name string, args []string) (*container, error) { 34 | c := &container{ 35 | name: name, 36 | } 37 | 38 | exec.Command("docker", "kill", "mediamtx-test-"+name).Run() 39 | exec.Command("docker", "wait", "mediamtx-test-"+name).Run() 40 | 41 | // --network=host is needed to test multicast 42 | cmd := []string{ 43 | "docker", "run", 44 | "--network=host", 45 | "--name=mediamtx-test-" + name, 46 | "mediamtx-test-" + image, 47 | } 48 | cmd = append(cmd, args...) 49 | ecmd := exec.Command(cmd[0], cmd[1:]...) 50 | ecmd.Stdout = nil 51 | ecmd.Stderr = os.Stderr 52 | 53 | err := ecmd.Start() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | time.Sleep(1 * time.Second) 59 | 60 | return c, nil 61 | } 62 | 63 | func (c *container) close() { 64 | exec.Command("docker", "kill", "mediamtx-test-"+c.name).Run() 65 | exec.Command("docker", "wait", "mediamtx-test-"+c.name).Run() 66 | exec.Command("docker", "rm", "mediamtx-test-"+c.name).Run() 67 | } 68 | 69 | func (c *container) wait() int { 70 | exec.Command("docker", "wait", "mediamtx-test-"+c.name).Run() 71 | out, _ := exec.Command("docker", "inspect", "mediamtx-test-"+c.name, 72 | "-f", "{{.State.ExitCode}}").Output() 73 | code, _ := strconv.ParseInt(string(out[:len(out)-1]), 10, 32) 74 | return int(code) 75 | } 76 | -------------------------------------------------------------------------------- /internal/unit/ac3.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // AC3 is a AC-3 data unit. 4 | type AC3 struct { 5 | Base 6 | Frames [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/av1.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // AV1 is an AV1 data unit. 4 | type AV1 struct { 5 | Base 6 | TU [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/base.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pion/rtp" 7 | ) 8 | 9 | // Base contains fields shared across all units. 10 | type Base struct { 11 | RTPPackets []*rtp.Packet 12 | NTP time.Time 13 | PTS int64 14 | } 15 | 16 | // GetRTPPackets implements Unit. 17 | func (u *Base) GetRTPPackets() []*rtp.Packet { 18 | return u.RTPPackets 19 | } 20 | 21 | // GetNTP implements Unit. 22 | func (u *Base) GetNTP() time.Time { 23 | return u.NTP 24 | } 25 | 26 | // GetPTS implements Unit. 27 | func (u *Base) GetPTS() int64 { 28 | return u.PTS 29 | } 30 | -------------------------------------------------------------------------------- /internal/unit/g711.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // G711 is a G711 data unit. 4 | type G711 struct { 5 | Base 6 | Samples []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/generic.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // Generic is a generic data unit. 4 | type Generic struct { 5 | Base 6 | } 7 | -------------------------------------------------------------------------------- /internal/unit/h264.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // H264 is a H264 data unit. 4 | type H264 struct { 5 | Base 6 | AU [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/h265.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // H265 is a H265 data unit. 4 | type H265 struct { 5 | Base 6 | AU [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/lpcm.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // LPCM is a LPCM data unit. 4 | type LPCM struct { 5 | Base 6 | Samples []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/mjpeg.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // MJPEG is a M-JPEG data unit. 4 | type MJPEG struct { 5 | Base 6 | Frame []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/mpeg1_audio.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // MPEG1Audio is a MPEG-1/2 Audio data unit. 4 | type MPEG1Audio struct { 5 | Base 6 | Frames [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/mpeg1_video.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // MPEG1Video is a MPEG-1/2 Video data unit. 4 | type MPEG1Video struct { 5 | Base 6 | Frame []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/mpeg4_audio.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // MPEG4Audio is a MPEG-4 Audio data unit. 4 | type MPEG4Audio struct { 5 | Base 6 | AUs [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/mpeg4_video.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // MPEG4Video is a MPEG-4 Video data unit. 4 | type MPEG4Video struct { 5 | Base 6 | Frame []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/opus.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // Opus is a Opus data unit. 4 | type Opus struct { 5 | Base 6 | Packets [][]byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/unit.go: -------------------------------------------------------------------------------- 1 | // Package unit contains the Unit definition. 2 | package unit 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/pion/rtp" 8 | ) 9 | 10 | // Unit is the elementary data unit routed across the server. 11 | type Unit interface { 12 | // returns RTP packets contained into the unit. 13 | GetRTPPackets() []*rtp.Packet 14 | 15 | // returns the NTP timestamp of the unit. 16 | GetNTP() time.Time 17 | 18 | // returns the PTS of the unit. 19 | GetPTS() int64 20 | } 21 | -------------------------------------------------------------------------------- /internal/unit/vp8.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // VP8 is a VP8 data unit. 4 | type VP8 struct { 5 | Base 6 | Frame []byte 7 | } 8 | -------------------------------------------------------------------------------- /internal/unit/vp9.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // VP9 is a VP9 data unit. 4 | type VP9 struct { 5 | Base 6 | Frame []byte 7 | } 8 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluenviron/mediamtx/870b99f69ffcb2f65b30114d803511bd553e83f7/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // main executable. 2 | package main 3 | 4 | import ( 5 | "os" 6 | 7 | "github.com/bluenviron/mediamtx/internal/core" 8 | ) 9 | 10 | func main() { 11 | s, ok := core.New(os.Args[1:]) 12 | if !ok { 13 | os.Exit(1) 14 | } 15 | s.Wait() 16 | } 17 | -------------------------------------------------------------------------------- /scripts/apidocs.mk: -------------------------------------------------------------------------------- 1 | define DOCKERFILE_APIDOCS_GEN 2 | FROM $(NODE_IMAGE) 3 | RUN yarn global add redoc-cli@0.13.7 4 | endef 5 | export DOCKERFILE_APIDOCS_GEN 6 | 7 | apidocs: 8 | echo "$$DOCKERFILE_APIDOCS_GEN" | docker build . -f - -t temp 9 | docker run --rm -v "$(shell pwd)/apidocs:/s" -w /s temp \ 10 | sh -c "redoc-cli bundle openapi.yaml" 11 | -------------------------------------------------------------------------------- /scripts/dockerhub.mk: -------------------------------------------------------------------------------- 1 | DOCKER_REPOSITORY = bluenviron/mediamtx 2 | 3 | dockerhub: 4 | $(eval VERSION := $(shell git describe --tags | tr -d v)) 5 | 6 | docker login -u $(DOCKER_USER) -p $(DOCKER_PASSWORD) 7 | 8 | docker buildx rm builder 2>/dev/null || true 9 | docker buildx create --name=builder 10 | 11 | docker build --builder=builder \ 12 | -f docker/ffmpeg-rpi.Dockerfile . \ 13 | --platform=linux/arm/v6,linux/arm/v7,linux/arm64 \ 14 | -t $(DOCKER_REPOSITORY):$(VERSION)-ffmpeg-rpi \ 15 | -t $(DOCKER_REPOSITORY):latest-ffmpeg-rpi \ 16 | --push 17 | 18 | docker build --builder=builder \ 19 | -f docker/rpi.Dockerfile . \ 20 | --platform=linux/arm/v6,linux/arm/v7,linux/arm64 \ 21 | -t $(DOCKER_REPOSITORY):$(VERSION)-rpi \ 22 | -t $(DOCKER_REPOSITORY):latest-rpi \ 23 | --push 24 | 25 | docker build --builder=builder \ 26 | -f docker/ffmpeg.Dockerfile . \ 27 | --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \ 28 | -t $(DOCKER_REPOSITORY):$(VERSION)-ffmpeg \ 29 | -t $(DOCKER_REPOSITORY):latest-ffmpeg \ 30 | --push 31 | 32 | docker build --builder=builder \ 33 | -f docker/standard.Dockerfile . \ 34 | --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \ 35 | -t $(DOCKER_REPOSITORY):$(VERSION) \ 36 | -t $(DOCKER_REPOSITORY):latest \ 37 | --push 38 | 39 | docker buildx rm builder 40 | -------------------------------------------------------------------------------- /scripts/format.mk: -------------------------------------------------------------------------------- 1 | define DOCKERFILE_FORMAT 2 | FROM $(BASE_IMAGE) 3 | RUN go install mvdan.cc/gofumpt@v0.5.0 4 | endef 5 | export DOCKERFILE_FORMAT 6 | 7 | format: 8 | echo "$$DOCKERFILE_FORMAT" | docker build -q . -f - -t temp 9 | docker run --rm -it -v "$(shell pwd):/s" -w /s temp \ 10 | sh -c "gofumpt -l -w ." 11 | -------------------------------------------------------------------------------- /scripts/lint.mk: -------------------------------------------------------------------------------- 1 | define DOCKERFILE_APIDOCS_LINT 2 | FROM $(NODE_IMAGE) 3 | RUN yarn global add @redocly/cli@1.0.0-beta.123 4 | endef 5 | export DOCKERFILE_APIDOCS_LINT 6 | 7 | lint-golangci: 8 | docker run --rm -v "$(shell pwd):/app" -w /app \ 9 | $(LINT_IMAGE) \ 10 | golangci-lint run -v 11 | 12 | lint-mod-tidy: 13 | go mod tidy 14 | git diff --exit-code 15 | 16 | lint-apidocs: 17 | echo "$$DOCKERFILE_APIDOCS_LINT" | docker build . -f - -t temp 18 | docker run --rm -v "$(shell pwd)/apidocs:/s" -w /s temp \ 19 | sh -c "openapi lint openapi.yaml" 20 | 21 | lint: lint-golangci lint-mod-tidy lint-apidocs 22 | -------------------------------------------------------------------------------- /scripts/mod-tidy.mk: -------------------------------------------------------------------------------- 1 | mod-tidy: 2 | docker run --rm -it -v "$(shell pwd):/s" -w /s $(BASE_IMAGE) \ 3 | sh -c "apk add git && GOPROXY=direct go mod tidy" 4 | -------------------------------------------------------------------------------- /scripts/run.mk: -------------------------------------------------------------------------------- 1 | define DOCKERFILE_RUN 2 | FROM $(BASE_IMAGE) 3 | RUN apk add --no-cache ffmpeg 4 | WORKDIR /s 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | COPY . ./ 8 | RUN go generate ./... 9 | RUN go build -o /out . 10 | WORKDIR / 11 | ARG CONFIG_RUN 12 | RUN echo "$$CONFIG_RUN" > mediamtx.yml 13 | endef 14 | export DOCKERFILE_RUN 15 | 16 | define CONFIG_RUN 17 | #rtspAddress: :8555 18 | #rtpAddress: :8002 19 | #rtcpAddress: :8003 20 | #metrics: yes 21 | #pprof: yes 22 | 23 | paths: 24 | all: 25 | # runOnReady: ffmpeg -i rtsp://localhost:$$RTSP_PORT/$$MTX_PATH -c copy -f mpegts myfile_$$MTX_PATH.ts 26 | # readUser: test 27 | # readPass: tast 28 | # runOnDemand: ffmpeg -re -stream_loop -1 -i testimages/ffmpeg/emptyvideo.mkv -c copy -f rtsp rtsp://localhost:$$RTSP_PORT/$$MTX_PATH 29 | 30 | # proxied: 31 | # source: rtsp://192.168.2.198:554/stream 32 | # rtspTransport: tcp 33 | # sourceOnDemand: yes 34 | # runOnDemand: ffmpeg -i rtsp://192.168.2.198:554/stream -c copy -f rtsp rtsp://localhost:$$RTSP_PORT/proxied2 35 | 36 | # original: 37 | # runOnReady: ffmpeg -i rtsp://localhost:554/original -b:a 64k -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:8554/compressed 38 | 39 | endef 40 | export CONFIG_RUN 41 | 42 | run: 43 | echo "$$DOCKERFILE_RUN" | docker build -q . -f - -t temp \ 44 | --build-arg CONFIG_RUN="$$CONFIG_RUN" 45 | docker run --rm -it \ 46 | --network=host \ 47 | temp \ 48 | sh -c "/out" 49 | -------------------------------------------------------------------------------- /scripts/test-e2e.mk: -------------------------------------------------------------------------------- 1 | test-e2e-nodocker: 2 | go generate ./... 3 | go test -v -race -tags enable_e2e_tests ./internal/teste2e 4 | 5 | define DOCKERFILE_E2E_TEST 6 | FROM $(BASE_IMAGE) 7 | RUN apk add --no-cache make docker-cli gcc musl-dev 8 | WORKDIR /s 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | COPY . ./ 12 | endef 13 | export DOCKERFILE_E2E_TEST 14 | 15 | test-e2e: 16 | echo "$$DOCKERFILE_E2E_TEST" | docker build -q . -f - -t temp 17 | docker run --rm -it \ 18 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 19 | --network=host \ 20 | temp \ 21 | make test-e2e-nodocker 22 | -------------------------------------------------------------------------------- /scripts/test.mk: -------------------------------------------------------------------------------- 1 | ifeq ($(shell getconf LONG_BIT),64) 2 | RACE=-race 3 | endif 4 | 5 | test-internal: 6 | go generate ./... 7 | go test -v $(RACE) -coverprofile=coverage-internal.txt \ 8 | $$(go list ./internal/... | grep -v /core) 9 | 10 | test-core: 11 | go test -v $(RACE) -coverprofile=coverage-core.txt ./internal/core 12 | 13 | test-nodocker: test-internal test-core 14 | 15 | define DOCKERFILE_TEST 16 | ARG ARCH 17 | FROM $$ARCH/$(BASE_IMAGE) 18 | RUN apk add --no-cache make gcc musl-dev 19 | WORKDIR /s 20 | COPY go.mod go.sum ./ 21 | RUN go mod download 22 | endef 23 | export DOCKERFILE_TEST 24 | 25 | test: 26 | echo "$$DOCKERFILE_TEST" | docker build -q . -f - -t temp --build-arg ARCH=amd64 27 | docker run --rm \ 28 | -v "$(shell pwd):/s" \ 29 | temp \ 30 | make test-nodocker 31 | 32 | test-32: 33 | echo "$$DOCKERFILE_TEST" | docker build -q . -f - -t temp --build-arg ARCH=i386 34 | docker run --rm \ 35 | -v "$(shell pwd):/s" \ 36 | temp \ 37 | make test-nodocker 38 | --------------------------------------------------------------------------------