From 970713a3c596628a58d7ecbc0bd810db4f363daf Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sat, 24 Jan 2026 22:21:08 +0100 Subject: [PATCH 1/4] Build binaries using burrito --- mix.exs | 19 +++++++++++++++++++ mix.lock | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/mix.exs b/mix.exs index 8ebb00f..2c96bc4 100644 --- a/mix.exs +++ b/mix.exs @@ -12,6 +12,7 @@ defmodule Shinkai.MixProject do elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), + releases: releases(), # hex description: "Media server for Elixir", package: package(), @@ -36,6 +37,7 @@ defmodule Shinkai.MixProject do {:hlx, "~> 0.5.0"}, {:ex_rtmp, "~> 0.4.1"}, {:yaml_elixir, "~> 2.12"}, + {:burrito, "~> 1.5.0"}, {:plug, "~> 1.19", optional: true}, {:bandit, "~> 1.8", optional: true}, {:ex_doc, "~> 0.30", only: :dev, runtime: false}, @@ -46,6 +48,23 @@ defmodule Shinkai.MixProject do defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] + def releases do + [ + shinkai: [ + steps: [:assemble, &Burrito.wrap/1], + burrito: [ + targets: [ + macos: [os: :darwin, cpu: :x86_64], + macos_silicon: [os: :darwin, cpu: :aarch64], + linux: [os: :linux, cpu: :x86_64], + linux_arm: [os: :linux, cpu: :aarch64], + windows: [os: :windows, cpu: :x86_64] + ] + ] + ] + ] + end + defp package do [ maintainers: ["Billal Ghilas"], diff --git a/mix.lock b/mix.lock index 3e1d075..11120cd 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"}, "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "burrito": {:hex, :burrito, "1.5.0", "d68ec01df2871f1d5bc603b883a78546c75761ac73c1bec1b7ae2cc74790fcd1", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "3861abda7bffa733862b48da3e03df0b4cd41abf6fd24b91745f5c16d971e5fa"}, "coerce": {:hex, :coerce, "1.0.2", "5ef791040c92baaa5dd344887563faaeac6e6742573a167493294f8af3672bbe", [:mix], [], "hexpm", "0b3451c729571234fdac478636c298e71d1f2ce1243abed5fa43fa3181b980eb"}, "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, @@ -16,6 +17,7 @@ "ex_sdp": {:hex, :ex_sdp, "1.1.2", "7e7465cb13b557cc76ef3e854bad7626b73cc1d1f480d38b5fbcf539c7d8a45d", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "50a27c2d745924679acca32b3d5499d0b35d135a180b83422df82c289afce564"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "hlx": {:hex, :hlx, "0.5.0", "22542ba77c4fd9a50d4566e546ff49b19d9b22d5110bf3b920d3d1f5f61ca9c1", [:mix], [{:ex_m3u8, "~> 0.15.0", [hex: :ex_m3u8, repo: "hexpm", optional: false]}, {:ex_mp4, "~> 0.14.0", [hex: :ex_mp4, repo: "hexpm", optional: false]}, {:media_codecs, "~> 0.10.0", [hex: :media_codecs, repo: "hexpm", optional: false]}, {:mpeg_ts, "~> 3.3.5", [hex: :mpeg_ts, repo: "hexpm", optional: false]}, {:qex, "~> 0.5.1", [hex: :qex, repo: "hexpm", optional: false]}], "hexpm", "74bcb44fda7a2407b37c125385b2389b7922438aba05b13bbc6ab01f8ba42a70"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -24,10 +26,12 @@ "media_codecs": {:hex, :media_codecs, "0.10.0", "dcc64779c3b287202fd8083fe49bf11b37f7b6bbd8edf3a9bd756370ee4417c5", [:mix], [], "hexpm", "8ea233ae378acfae3ab95a90f6f5c99711d55f15d0c5fac244d46b42f6a9ca04"}, "membrane_rtsp": {:hex, :membrane_rtsp, "0.11.0", "887b1c0cd4f40f6ce93880bfa1a1e8c9e250aabb24810a8fe2a7556bb54c29c4", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:ex_sdp, "~> 0.17.0 or ~> 1.0", [hex: :ex_sdp, repo: "hexpm", optional: false]}, {:mockery, "~> 2.3", [hex: :mockery, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "69252d77ad3df48e6cb21fc16b0c5730607709714ad7849b7635813f9741ee2f"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mockery": {:hex, :mockery, "2.5.0", "a87acd74fd733aa3b9cb5663d6f690178b056608f2652f18e4ec423ddd5496ed", [:mix], [], "hexpm", "52492b2eba61055df1c626e894663b624b5e6fdfaaaba1d9a8596236fbf4da69"}, "mpeg_ts": {:hex, :mpeg_ts, "3.3.11", "77d69c5599fcd6eadef926b03cf6fe990dd76301ec41ce77de71bc84ad53412c", [:mix], [], "hexpm", "e1554e7b2ffe5692effca19173200fdee0959bd40d201ec920a950054c27cb76"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, @@ -35,6 +39,7 @@ "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"}, "rtsp": {:hex, :rtsp, "0.8.1", "4bffebfcb0e1354283567178c040bbf40a85c4fbbde6d23addbbc7672cb3c700", [:mix], [{:ex_mp4, "~> 0.14.0", [hex: :ex_mp4, repo: "hexpm", optional: true]}, {:ex_rtcp, "~> 0.4.0", [hex: :ex_rtcp, repo: "hexpm", optional: false]}, {:ex_rtp, "~> 0.4.0", [hex: :ex_rtp, repo: "hexpm", optional: false]}, {:ex_sdp, "~> 1.0", [hex: :ex_sdp, repo: "hexpm", optional: false]}, {:media_codecs, "~> 0.10.0", [hex: :media_codecs, repo: "hexpm", optional: false]}, {:membrane_rtsp, "~> 0.11.0", [hex: :membrane_rtsp, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "b4af3c30b8f79dd642940452c6ad6727bfd1df492e5ddbc1eb705f25df6f4053"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, From 2805840e116319486df216b3ac10613d9f726407 Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sun, 25 Jan 2026 14:05:51 +0100 Subject: [PATCH 2/4] Add release github action --- .github/workflows/release.yml | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c80baba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + tags: + - "v*" + +env: + MIX_ENV: prod + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-elixir@v1 + with: + elixir-version: "1.18.4" + otp-version: "27.3.4.1" + + - uses: mlugg/setup-zig@v2 + with: + version: "0.15.2" + + - name: Install dependencies + run: mix deps.get + + - name: Build release + run: mix release + + - name: Create Checksum + run: | + chmod +x ./burrito_out/* + shasum -a 256 ./burrito_out/* > shinkai_checksums.txt + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + with: + tag_name: ${{ github.ref_name }} + release_name: Release ${{ github.ref_name }} + draft: true + + - name: Publish archives and packages + uses: softprops/action-gh-release@v1 + with: + draft: true + generate_release_notes: true + files: | + ./burrito_out/* + ./shinkai_checksums.txt From 627a1c37cf4b16d7ee43a7bc5cba73babd759d3a Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Mon, 26 Jan 2026 11:19:17 +0100 Subject: [PATCH 3/4] Add more tests --- lib/shinkai/sources/rtmp/media_processor.ex | 93 ++++++++++++-------- test/shinkai/sources/rtmp_test.exs | 84 +++++++++--------- test/shinkai/sources/rtsp_test.exs | 97 +++++++++------------ test/support/media_assertion.ex | 71 +++++++++++++++ 4 files changed, 209 insertions(+), 136 deletions(-) create mode 100644 test/support/media_assertion.ex diff --git a/lib/shinkai/sources/rtmp/media_processor.ex b/lib/shinkai/sources/rtmp/media_processor.ex index 6b9f462..62cb36c 100644 --- a/lib/shinkai/sources/rtmp/media_processor.ex +++ b/lib/shinkai/sources/rtmp/media_processor.ex @@ -37,38 +37,21 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do @spec handle_video_data(tuple(), state()) :: state() def handle_video_data({:codec, codec, init_data}, state) do - track = Track.new(id: 1, type: :video, codec: codec, timescale: @timescale) - track = - case codec do - :h264 -> - avcc = ExMP4.Box.parse(%ExMP4.Box.Avcc{}, init_data) - %{track | codec: :h264, priv_data: {List.first(avcc.sps), avcc.pps}} - - :h265 -> - hvcc = ExMP4.Box.parse(%ExMP4.Box.Hvcc{}, init_data) - - %{ - track - | codec: :h265, - priv_data: {List.first(hvcc.vps), List.first(hvcc.sps), hvcc.pps} - } - - :av1 -> - av1c = ExMP4.Box.parse(%ExMP4.Box.Av1c{}, init_data) - priv_data = if av1c.config_obus != <<>>, do: av1c.config_obus - %{track | codec: :av1, priv_data: priv_data} - - _ -> - track - end + Track.new( + id: 1, + type: :video, + codec: codec, + timescale: 90_000, + priv_data: track_priv_data(codec, init_data) + ) state = %{state | video_track: track} if state.audio_track, do: unbuffer(state), else: state end def handle_video_data(sample, %{buffer?: false} = state) do - packet = packet_from_sample(state.video_track.id, sample) + packet = packet_from_sample(state.video_track, sample) PubSub.broadcast(Shinkai.PubSub, state.packets_topic, {:packet, packet}) state end @@ -80,16 +63,28 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do def handle_video_data(sample, state) do %{ state - | packets: [packet_from_sample(state.video_track.id, sample) | state.packets], + | packets: [packet_from_sample(state.video_track, sample) | state.packets], buffer_len: state.buffer_len + 1 } end @spec handle_audio_data(tuple(), state()) :: state() def handle_audio_data({:codec, codec, init_data}, state) do - track = Track.new(id: 2, type: :audio, codec: codec, timescale: @timescale) + track = + Track.new( + id: 2, + type: :audio, + codec: codec, + timescale: @timescale, + priv_data: track_priv_data(codec, init_data) + ) - track = if codec == :aac, do: %{track | priv_data: init_data}, else: track + track = + case track.codec do + :aac -> %{track | timescale: track.priv_data.sampling_frequency} + :opus -> %{track | timescale: 48_000} + _codec -> track + end state = %{state | audio_track: track} if state.video_track, do: unbuffer(state), else: state @@ -99,7 +94,7 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do PubSub.broadcast( Shinkai.PubSub, state.packets_topic, - {:packet, packet_from_sample(state.audio_track.id, sample)} + {:packet, packet_from_sample(state.audio_track, sample)} ) state @@ -112,7 +107,7 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do def handle_audio_data(sample, state) do %{ state - | packets: [packet_from_sample(state.audio_track.id, sample) | state.packets], + | packets: [packet_from_sample(state.audio_track, sample) | state.packets], buffer_len: state.buffer_len + 1 } end @@ -135,20 +130,46 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do %{state | buffer?: false, packets: [], buffer_len: 0} end + defp track_priv_data(:h264, init_data) do + avcc = ExMP4.Box.parse(%ExMP4.Box.Avcc{}, init_data) + {List.first(avcc.sps), avcc.pps} + end + + defp track_priv_data(:h265, init_data) do + hvcc = ExMP4.Box.parse(%ExMP4.Box.Hvcc{}, init_data) + {List.first(hvcc.vps), List.first(hvcc.sps), hvcc.pps} + end + + defp track_priv_data(:av1, init_data) do + av1c = ExMP4.Box.parse(%ExMP4.Box.Av1c{}, init_data) + + if av1c.config_obus != <<>>, do: av1c.config_obus + end + + defp track_priv_data(:aac, init_data) do + MediaCodecs.MPEG4.AudioSpecificConfig.parse(init_data) + end + + defp track_priv_data(:opus, _init_data), do: nil + + defp track_priv_data(_codec, init_data), do: init_data + @compile {:inline, packet_from_sample: 2} - defp packet_from_sample(track_id, {:sample, payload, dts, pts, sync?}) do + defp packet_from_sample(track, {:sample, payload, dts, pts, sync?}) do %Packet{ - track_id: track_id, + track_id: track.id, data: payload, - dts: dts, - pts: pts, + dts: div(dts * track.timescale, @timescale), + pts: div(pts * track.timescale, @timescale), sync?: sync? } end - defp packet_from_sample(track_id, {:sample, payload, pts}) do + defp packet_from_sample(track, {:sample, payload, pts}) do + pts = div(pts * track.timescale, @timescale) + %Packet{ - track_id: track_id, + track_id: track.id, data: payload, dts: pts, pts: pts, diff --git a/test/shinkai/sources/rtmp_test.exs b/test/shinkai/sources/rtmp_test.exs index a6e318b..d1d9983 100644 --- a/test/shinkai/sources/rtmp_test.exs +++ b/test/shinkai/sources/rtmp_test.exs @@ -1,6 +1,8 @@ defmodule Shinkai.Sources.RTMPTest do use ExUnit.Case, async: true + import Shinkai.Test.MediaAssertion + alias ExRTMP.Server alias Shinkai.Sources alias Shinkai.Sources.Source @@ -16,57 +18,49 @@ defmodule Shinkai.Sources.RTMPTest do %{rtmp_server: server} end - test "tracks received from rtmp source", %{rtmp_server: server} do - {:ok, port} = Server.port(server) - - source = %Source{id: UUID.uuid4(), type: :rtmp, uri: "rtmp://127.0.0.1:#{port}/live/test"} - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.tracks_topic(source.id)) - - _pid = start_supervised!({Sources.RTMP, source}) - - assert_receive {:tracks, tracks}, 2_000 - assert length(tracks) == 2 - - assert [ - %Shinkai.Track{ - id: 1, - type: :video, - codec: :h264, - timescale: 1000, - priv_data: {sps, pps} - }, - %Shinkai.Track{ - id: 2, - type: :audio, - codec: :aac, - timescale: 1000, - priv_data: - <<17, 128, 4, 200, 68, 0, 32, 0, 196, 13, 76, 97, 118, 99, 53, 56, 46, 53, 52, - 46, 49, 48, 48, 86, 229, 0>> - } - ] = tracks - - assert is_binary(sps) - assert length(pps) == 1 - end + for fixture <- fixtures() do + test "tracks received from rtmp source: #{fixture}" do + server = start_server(unquote(fixture)) + {:ok, port} = Server.port(server) + + source = %Source{id: UUID.uuid4(), type: :rtmp, uri: "rtmp://127.0.0.1:#{port}/live/test"} + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.tracks_topic(source.id)) + + _pid = start_supervised!({Sources.RTMP, source}) - test "packets are received from rtmp source", %{rtmp_server: server} do - {:ok, port} = Server.port(server) + assert_receive {:tracks, tracks}, 2_000 + assert length(tracks) == 2 + assert_tracks(unquote(fixture), tracks) + end + + test "packets are received from rtmp source: #{fixture}" do + server = start_server(unquote(fixture)) + {:ok, port} = Server.port(server) - source = %Source{id: UUID.uuid4(), type: :rtmp, uri: "rtmp://127.0.0.1:#{port}/live/test"} - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.packets_topic(source.id)) - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.state_topic(source.id)) + source = %Source{id: UUID.uuid4(), type: :rtmp, uri: "rtmp://127.0.0.1:#{port}/live/test"} + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.packets_topic(source.id)) + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.state_topic(source.id)) - _pid = start_supervised!({Sources.RTMP, source}) + _pid = start_supervised!({Sources.RTMP, source}) - packets = collect_packets() - assert length(packets) == 770 - assert Enum.filter(packets, &(&1.track_id == 1)) |> length() == 300 - assert Enum.filter(packets, &(&1.track_id == 2)) |> length() == 470 + packets = collect_packets() + assert_received_packets(unquote(fixture), packets) - Server.stop(server) + Server.stop(server) + + assert_receive :disconnected, 1_000 + end + end + + defp start_server(fixture) do + {:ok, server} = + Server.start( + port: 0, + handler: Shinkai.RTMP.Server.Handler, + handler_options: [fixture: fixture] + ) - assert_receive :disconnected, 1_000 + server end defp collect_packets(acc \\ []) do diff --git a/test/shinkai/sources/rtsp_test.exs b/test/shinkai/sources/rtsp_test.exs index d363616..8c34eb6 100644 --- a/test/shinkai/sources/rtsp_test.exs +++ b/test/shinkai/sources/rtsp_test.exs @@ -1,75 +1,62 @@ defmodule Shinkai.Sources.RTSPTest do use ExUnit.Case, async: true + import Shinkai.Test.MediaAssertion + alias RTSP.FileServer alias Shinkai.Sources.Source setup do - {:ok, server} = - FileServer.start_link( - port: 0, - files: [%{path: "/test", location: "test/fixtures/big_buck_avc_aac.mp4"}], - rate_control: false - ) + files = + fixtures() + |> Enum.with_index(1) + |> Enum.map(fn {fixture, index} -> %{path: "/test#{index}", location: fixture} end) + + {:ok, server} = FileServer.start_link(port: 0, files: files, rate_control: false) %{rtsp_server: server} end - test "tracks received from rtsp source", %{rtsp_server: server} do - {:ok, port} = FileServer.port_number(server) - - source = %Source{id: "test", type: :rtsp, uri: "rtsp://127.0.0.1:#{port}/test"} - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.tracks_topic(source.id)) - - _pid = start_supervised!({Shinkai.Sources.RTSP, source}) - - assert_receive {:tracks, tracks}, 2_000 - assert length(tracks) == 2 - - assert [ - %Shinkai.Track{ - id: 1, - type: :video, - codec: :h264, - timescale: 90_000, - priv_data: - {<<103, 66, 192, 12, 217, 3, 196, 254, 95, 252, 2, 32, 2, 28, 64, 0, 0, 3, 0, 64, - 0, 0, 15, 3, 197, 10, 146>>, [<<104, 203, 131, 203, 32>>]} - }, - %Shinkai.Track{ - id: 2, - type: :audio, - codec: :aac, - timescale: 48_000, - priv_data: %MediaCodecs.MPEG4.AudioSpecificConfig{ - object_type: 2, - sampling_frequency: 48_000, - channels: 0, - aot_specific_config: - <<0, 153, 8, 128, 4, 0, 24, 129, 169, 140, 46, 204, 102, 167, 5, 198, 166, 133, - 198, 38, 6, 10, 220, 160, 0::size(3)>> - } - } - ] == tracks - end + for {fixture, index} <- Enum.with_index(fixtures(), 1) do + test "tracks received from rtsp source: #{fixture}", %{rtsp_server: server} do + {:ok, port} = FileServer.port_number(server) + + source = %Source{ + id: "test", + type: :rtsp, + uri: "rtsp://127.0.0.1:#{port}/test#{unquote(index)}" + } + + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.tracks_topic(source.id)) - test "packets are received from rtsp source", %{rtsp_server: server} do - {:ok, port} = FileServer.port_number(server) + _pid = start_supervised!({Shinkai.Sources.RTSP, source}) - source = %Source{id: UUID.uuid4(), type: :rtsp, uri: "rtsp://127.0.0.1:#{port}/test"} - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.packets_topic(source.id)) - Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.state_topic(source.id)) + assert_receive {:tracks, tracks}, 2_000 + assert length(tracks) == 2 + assert_tracks(unquote(fixture), tracks) + end + + test "packets are received from rtsp source: #{fixture}", %{rtsp_server: server} do + {:ok, port} = FileServer.port_number(server) + + source = %Source{ + id: UUID.uuid4(), + type: :rtsp, + uri: "rtsp://127.0.0.1:#{port}/test#{unquote(index)}" + } - _pid = start_supervised!({Shinkai.Sources.RTSP, source}) + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.packets_topic(source.id)) + Phoenix.PubSub.subscribe(Shinkai.PubSub, Shinkai.Utils.state_topic(source.id)) - packets = collect_packets() - assert length(packets) == 770 - assert Enum.filter(packets, &(&1.track_id == 1)) |> length() == 300 - assert Enum.filter(packets, &(&1.track_id == 2)) |> length() == 470 + _pid = start_supervised!({Shinkai.Sources.RTSP, source}) - FileServer.stop(server) + packets = collect_packets() + assert_received_packets(unquote(fixture), packets) - assert_receive :disconnected, 1_000 + FileServer.stop(server) + + assert_receive :disconnected, 1_000 + end end defp collect_packets(acc \\ []) do diff --git a/test/support/media_assertion.ex b/test/support/media_assertion.ex new file mode 100644 index 0000000..ea8d059 --- /dev/null +++ b/test/support/media_assertion.ex @@ -0,0 +1,71 @@ +defmodule Shinkai.Test.MediaAssertion do + use ExUnit.Case + + alias MediaCodecs.AV1 + + @fixtures ["test/fixtures/big_buck_avc_aac.mp4", "test/fixtures/big_buck_av1_opus.mp4"] + + def fixtures(), do: @fixtures + + def assert_tracks("test/fixtures/big_buck_avc_aac.mp4", tracks) do + assert [ + %Shinkai.Track{ + id: 1, + type: :video, + codec: :h264, + timescale: 90_000, + priv_data: + {<<103, 66, 192, 12, 217, 3, 196, 254, 95, 252, 2, 32, 2, 28, 64, 0, 0, 3, 0, 64, + 0, 0, 15, 3, 197, 10, 146>>, [<<104, 203, 131, 203, 32>>]} + }, + %Shinkai.Track{ + id: 2, + type: :audio, + codec: :aac, + timescale: 48_000, + priv_data: %MediaCodecs.MPEG4.AudioSpecificConfig{ + object_type: 2, + sampling_frequency: 48_000, + channels: 0, + aot_specific_config: + <<0, 153, 8, 128, 4, 0, 24, 129, 169, 140, 46, 204, 102, 167, 5, 198, 166, 133, + 198, 38, 6, 10, 220, 160, 0::size(3)>> + } + } + ] == tracks + end + + def assert_tracks("test/fixtures/big_buck_av1_opus.mp4", tracks) do + assert [ + %Shinkai.Track{ + id: 1, + type: :video, + codec: :av1, + timescale: 90_000, + priv_data: config_obu + }, + %Shinkai.Track{ + id: 2, + type: :audio, + codec: :opus, + timescale: 48_000, + priv_data: opus_priv_data + } + ] = tracks + + assert is_nil(opus_priv_data) or is_binary(opus_priv_data) + assert {:ok, %{header: %{type: :sequence_header}}} = AV1.OBU.parse(config_obu) + end + + def assert_received_packets("test/fixtures/big_buck_avc_aac.mp4", packets) do + assert length(packets) == 770 + assert Enum.filter(packets, &(&1.track_id == 1)) |> length() == 300 + assert Enum.filter(packets, &(&1.track_id == 2)) |> length() == 470 + end + + def assert_received_packets("test/fixtures/big_buck_av1_opus.mp4", packets) do + assert length(packets) == 801 + assert Enum.filter(packets, &(&1.track_id == 1)) |> length() == 300 + assert Enum.filter(packets, &(&1.track_id == 2)) |> length() == 501 + end +end From 339a9672cb7851dd68ba9e87a29019a27ff60040 Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Mon, 26 Jan 2026 11:21:27 +0100 Subject: [PATCH 4/4] make credo happy --- lib/shinkai/sources/rtmp/media_processor.ex | 3 ++- test/support/media_assertion.ex | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/shinkai/sources/rtmp/media_processor.ex b/lib/shinkai/sources/rtmp/media_processor.ex index 62cb36c..350e53c 100644 --- a/lib/shinkai/sources/rtmp/media_processor.ex +++ b/lib/shinkai/sources/rtmp/media_processor.ex @@ -5,6 +5,7 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do require Logger + alias MediaCodecs.MPEG4 alias Phoenix.PubSub alias Shinkai.{Packet, Track} @@ -147,7 +148,7 @@ defmodule Shinkai.Sources.RTMP.MediaProcessor do end defp track_priv_data(:aac, init_data) do - MediaCodecs.MPEG4.AudioSpecificConfig.parse(init_data) + MPEG4.AudioSpecificConfig.parse(init_data) end defp track_priv_data(:opus, _init_data), do: nil diff --git a/test/support/media_assertion.ex b/test/support/media_assertion.ex index ea8d059..e291de7 100644 --- a/test/support/media_assertion.ex +++ b/test/support/media_assertion.ex @@ -1,11 +1,13 @@ defmodule Shinkai.Test.MediaAssertion do + @moduledoc false + use ExUnit.Case alias MediaCodecs.AV1 @fixtures ["test/fixtures/big_buck_avc_aac.mp4", "test/fixtures/big_buck_av1_opus.mp4"] - def fixtures(), do: @fixtures + def fixtures, do: @fixtures def assert_tracks("test/fixtures/big_buck_avc_aac.mp4", tracks) do assert [