From f7d8c20aa8cf6ee7a468b5d91780147329b0471c Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sun, 1 Feb 2026 20:24:08 +0100 Subject: [PATCH 1/2] hls: Add opus support --- lib/shinkai/sink/hls.ex | 2 +- mix.exs | 2 +- mix.lock | 6 +++--- test/shinkai/pipeline_test.exs | 22 +++++++++------------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/shinkai/sink/hls.ex b/lib/shinkai/sink/hls.ex index 9a44803..1bc6643 100644 --- a/lib/shinkai/sink/hls.ex +++ b/lib/shinkai/sink/hls.ex @@ -13,7 +13,7 @@ defmodule Shinkai.Sink.Hls do alias HLX.Writer alias Phoenix.PubSub - @supported_codecs [:h264, :h265, :av1, :aac] + @supported_codecs [:h264, :h265, :av1, :aac, :opus] def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: opts[:name]) diff --git a/mix.exs b/mix.exs index 657540f..6bde88d 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule Shinkai.MixProject do [ {:phoenix_pubsub, "~> 2.2"}, {:rtsp, "~> 0.8.0"}, - {:hlx, "~> 0.5.0"}, + {:hlx, "~> 0.6.0"}, {:ex_rtmp, "~> 0.4.1"}, {:yaml_elixir, "~> 2.12"}, {:burrito, "~> 1.5.0"}, diff --git a/mix.lock b/mix.lock index 11120cd..22d49de 100644 --- a/mix.lock +++ b/mix.lock @@ -9,15 +9,15 @@ "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "ex_doc": {:hex, :ex_doc, "0.40.0", "2635974389b80fd3ca61b0f993d459dad05b4a8f9b069dcfbbc5f6a8a6aef60e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "c040735250e2752b6e1102eeb4aa3f1dca74c316db873ae09f955d42136e7e5b"}, "ex_flv": {:hex, :ex_flv, "0.4.0", "9e43c833b5cbe3c6e21bb2651ae7650f3ec939eac8079f34efed8f813bf9133d", [:mix], [], "hexpm", "484f6990791e0c8862a88e4150f004deb067c7342e40b779c82d5c9a9c057969"}, - "ex_m3u8": {:hex, :ex_m3u8, "0.15.4", "66f6ec7e4fb7372c48032db1c2d4a3e6c2bbbde2d1d9a1098986e3caa0ab7a55", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "ec03aa516919e0c8ec202da55f609b763bd7960195a3388900090fcad270c873"}, + "ex_m3u8": {:hex, :ex_m3u8, "0.16.0", "29fc405a790d95bb320cbbbbaa1d6c2608f043bce02ad8d35e6489497ca4b2a0", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "a98a149c14b3929cfe163fd9d23edfa86ab0bc72bee5c1945e9dab058aa051d7"}, "ex_mp4": {:hex, :ex_mp4, "0.14.2", "c362b27c50fa8d5a16e4f5652963fcc47d5a61215eb729a0d6f8ec521575ed6d", [:mix], [{:media_codecs, "~> 0.10.0", [hex: :media_codecs, repo: "hexpm", optional: true]}, {:ratio, "~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:table_rex, "~> 4.0", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "3712c62a93ddde83419bb22e382a145c6527c8b002d8a22348202828022e1041"}, "ex_rtcp": {:hex, :ex_rtcp, "0.4.1", "e0f0c0baf329de92059e2afdc34d66d61f8b983f0801daa10f1a712360919e45", [:mix], [], "hexpm", "83ab3dffcffc6149eb404f1e1b1b62f755efd54c818e27c2c88e6ba3341c5d41"}, "ex_rtmp": {:hex, :ex_rtmp, "0.4.1", "1be8a1f75f2940d59ae07939218d1cdddac85de118370f0f001f816b8bac4576", [:mix], [{:ex_flv, "~> 0.4.0", [hex: :ex_flv, repo: "hexpm", optional: false]}], "hexpm", "58e1f993c575b6604e8256ed89887fcb7f7b80d0627e0bd71e9eea98bbe79766"}, "ex_rtp": {:hex, :ex_rtp, "0.4.0", "1f1b5c1440a904706011e3afbb41741f5da309ce251cb986690ce9fd82636658", [:mix], [], "hexpm", "0f72d80d5953a62057270040f0f1ee6f955c08eeae82ac659c038001d7d5a790"}, "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"}, + "hlx": {:hex, :hlx, "0.6.0", "bbe81afa5f95f869633d92dfc55b908ce8da0e3f72183d98471994c87cd92e59", [:mix], [{:ex_m3u8, "~> 0.16.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", "7da77127e408b20c736ac8281a6a1e9b336df712c711cb46f62c6d03db9539fb"}, "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"}, @@ -38,8 +38,8 @@ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "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"}, + "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"}, "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"}, diff --git a/test/shinkai/pipeline_test.exs b/test/shinkai/pipeline_test.exs index 9fdd384..5acfe40 100644 --- a/test/shinkai/pipeline_test.exs +++ b/test/shinkai/pipeline_test.exs @@ -37,8 +37,7 @@ defmodule Shinkai.PipelineTest do assert_receive {:hls, :done}, 5_000 hls_path = Path.join(Shinkai.Config.get_config(:hls)[:storage_dir], source.id) - audio? = not String.contains?(unquote(fixture), "opus") - assert_hls(hls_path, audio?) + assert_hls(hls_path, true) on_exit(fn -> File.rm_rf!(hls_path) end) end @@ -56,8 +55,7 @@ defmodule Shinkai.PipelineTest do assert_receive {:hls, :done}, 5_000 hls_path = Path.join(Shinkai.Config.get_config(:hls)[:storage_dir], id) - audio? = not String.contains?(unquote(fixture), "opus") - assert_hls(hls_path, audio?) + assert_hls(hls_path, true) File.rm_rf!(hls_path) end @@ -78,8 +76,7 @@ defmodule Shinkai.PipelineTest do assert_receive {:hls, :done}, 5_000 hls_path = Path.join(Shinkai.Config.get_config(:hls)[:storage_dir], source.id) - audio? = not String.contains?(unquote(fixture), "opus") - assert_hls(hls_path, audio?) + assert_hls(hls_path, true) ExRTMP.Server.stop(rtmp_server) File.rm_rf!(hls_path) @@ -121,8 +118,6 @@ defmodule Shinkai.PipelineTest do source = %Source{id: "live-#{id}", type: :rtmp, uri: rtmp_uri(rtmp_server, "live/#{id}")} start_source(source) - # _pid = start_supervised!({Shinkai.Pipeline, source}) - {:ok, pid} = ExRTMP.Client.start_link(uri: rtmp_uri(rtmp_server, "live"), stream_key: id) assert :ok = ExRTMP.Client.connect(pid) assert :ok = ExRTMP.Client.play(pid) @@ -168,7 +163,6 @@ defmodule Shinkai.PipelineTest do Enum.find(items, &is_struct(&1, ExM3U8.Tags.Media)) end - # codesc "avc1.42C00C,mp4a.40.2" if audio? do assert %{audio: "audio", codecs: _codecs, resolution: {240, 136}} = Enum.find(items, &is_struct(&1, ExM3U8.Tags.Stream)) @@ -178,10 +172,10 @@ defmodule Shinkai.PipelineTest do end if audio? do - assert_media_playlist(hls_path, "audio", 3, 5) + assert_media_playlist(hls_path, "audio", 2..3, 5) end - assert_media_playlist(hls_path, "video", 2, 5) + assert_media_playlist(hls_path, "video", 2..2, 5) end defp assert_rtmp_receive(pid, fixture) do @@ -218,7 +212,7 @@ defmodule Shinkai.PipelineTest do "rtmp://127.0.0.1:#{port}/#{path}" end - defp assert_media_playlist(hls_path, variant, target_duration, segments_count) do + defp assert_media_playlist(hls_path, variant, expected_target_duration, segments_count) do assert {:ok, playlist} = hls_path |> Path.join("#{variant}.m3u8") @@ -227,11 +221,13 @@ defmodule Shinkai.PipelineTest do assert %ExM3U8.MediaPlaylist{ info: %ExM3U8.MediaPlaylist.Info{ - target_duration: ^target_duration + target_duration: target_duration }, timeline: timeline } = playlist + assert target_duration in expected_target_duration + assert %Tags.MediaInit{uri: init_uri} = Enum.at(timeline, 0) segments = Enum.filter(timeline, &is_struct(&1, Tags.Segment)) assert length(segments) == segments_count From 2ad9f5781e2b2904fbbade35b0dbfc519aa16cfb Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Sun, 1 Feb 2026 20:25:47 +0100 Subject: [PATCH 2/2] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fcb05d..fd9a9e4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Live streams can be read from the server with: | Protocol | Variants | Video Codecs | Audio Codecs | |----------|----------|--------------|--------------| -| HLS | fMP4/mpeg-ts/Low Latency | H264, H265, AV1 | MPEG-4(AAC) | +| HLS | fMP4/mpeg-ts/Low Latency | H264, H265, AV1 | MPEG-4(AAC), Opus | | RTMP | - | H264, H265, AV1 | MPEG-4(AAC), G711(PCMA/PCMU), Opus | ## Usage and configuration