Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create server #1

Merged
merged 16 commits into from
Nov 4, 2024
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# Membrane Template Plugin

[![Hex.pm](https://img.shields.io/hexpm/v/membrane_template_plugin.svg)](https://hex.pm/packages/membrane_template_plugin)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_template_plugin)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_template_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_template_plugin)
[![Hex.pm](https://img.shields.io/hexpm/v/membrane_simple_rtsp_server.svg)](https://hex.pm/packages/membrane_simple_rtsp_server)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_simple_rtsp_server)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_simple_rtsp_server.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_simple_rtsp_server)

This repository contains a template for new plugins.

Check out different branches for other flavors of this template.

It's a part of the [Membrane Framework](https://membrane.stream).
A Simple RTSP server that serves a MP4 file

## Installation

The package can be installed by adding `membrane_template_plugin` to your list of dependencies in `mix.exs`:
The package can be installed by adding `membrane_simple_rtsp_server` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:membrane_template_plugin, "~> 0.1.0"}
{:membrane_simple_rtsp_server, "~> 0.1.0"}
]
end
```

## Usage

TODO
To serve a MP4 file run the following:
```elixir
Membrane.SimpleRTSPServer.start_link("path/to/file.mp4", port: 30001)
```

To receive and immediately play the stream you can use a tool like `ffplay`:
```sh
ffplay rtsp://localhost:30001
```

To receive the mp4 and store it you can use a tool like [Boombox](https://github.com/membraneframework/boombox):
```elixir
Boombox.run(input: "rtsp://localhost:30001", output: "output.mp4")
```

## Copyright and License

Expand Down
13 changes: 9 additions & 4 deletions lib/membrane/simple_rtsp_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ defmodule Membrane.SimpleRTSPServer do
Module for starting a simple RTSP Server that streams a MP4 file.
"""

@spec start_link(String.t(), :inet.port_number(), :inet.ip4_address()) :: GenServer.on_start()
def start_link(mp4_path, rtsp_port \\ 554, address \\ {127, 0, 0, 1}) do
@spec start_link(String.t(),
port: :inet.port_number(),
address: :inet.ip4_address()
) :: GenServer.on_start()
def start_link(mp4_path, opts) do
Membrane.RTSP.Server.start_link(
handler: Membrane.SimpleRTSPServer.Handler,
handler_config: %{mp4_path: mp4_path},
port: rtsp_port,
address: address
port: Keyword.get(opts, :port, 554),
address: Keyword.get(opts, :address, {127, 0, 0, 1}),
udp_rtp_port: 0,
udp_rtcp_port: 0
)
end
end
72 changes: 53 additions & 19 deletions lib/membrane/simple_rtsp_server/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Membrane.SimpleRTSPServer.Handler do
@video_pt 96
@video_clock_rate 90_000
@audio_pt 97
@audio_clock_rate 90_000
@audio_clock_rate 44_100

@impl true
def init(config) do
Expand All @@ -25,18 +25,20 @@ defmodule Membrane.SimpleRTSPServer.Handler do

@impl true
def handle_describe(_req, state) do
asc = get_audio_specific_config(state.mp4_path)

sdp = """
v=0
m=video 0 RTP/AVP #{@video_pt}
a=control:video
a=rtpmap:#{@video_pt} H264/#{@video_clock_rate}
a=fmtp:#{@video_pt} packetization-mode=1
m=audio 0 RTP/AVP #{@audio_pt}
a=control:audio
a=rtpmap:#{@audio_pt} mpeg4-generic/#{@audio_clock_rate}/2
a=fmtp:#{@audio_pt} streamtype=5; profile-level-id=5; mode=AAC-hbr; config=#{Base.encode16(asc)}; sizeLength=13; indexLength=3
"""

# m=audio 0 RTP/AVP #{@audio_pt}
# a=control:audio
# a=rtpmap:#{@audio_pt} MP4A-LATM/#{@audio_clock_rate}

response =
Response.new(200)
|> Response.with_header("Content-Type", "application/sdp")
Expand All @@ -52,20 +54,34 @@ defmodule Membrane.SimpleRTSPServer.Handler do

@impl true
def handle_play(configured_media_context, state) do
media_context = configured_media_context |> Map.values() |> List.first()

{client_rtp_port, _client_rtcp_port} = media_context.client_port

arg = %{
socket: state.socket,
ssrc: media_context.ssrc,
pt: @video_pt,
clock_rate: @video_clock_rate,
client_port: client_rtp_port,
client_ip: media_context.address,
server_rtp_socket: media_context.rtp_socket,
mp4_path: state.mp4_path
}
media_config =
Map.new(configured_media_context, fn {control_path, context} ->
{key, pt, clock_rate} =
case URI.new!(control_path) do
%URI{path: "/video"} -> {:video, @video_pt, @video_clock_rate}
%URI{path: "/audio"} -> {:audio, @audio_pt, @audio_clock_rate}
end

{client_rtp_port, _client_rtcp_port} = context.client_port

config = %{
ssrc: context.ssrc,
pt: pt,
clock_rate: clock_rate,
rtp_socket: context.rtp_socket,
client_address: context.address,
client_port: client_rtp_port
}

{key, config}
end)

arg =
%{
socket: state.socket,
mp4_path: state.mp4_path,
media_config: media_config
}

{:ok, _sup_pid, pipeline_pid} =
Membrane.SimpleRTSPServer.Pipeline.start_link(arg)
Expand All @@ -85,4 +101,22 @@ defmodule Membrane.SimpleRTSPServer.Handler do

@impl true
def handle_closed_connection(_state), do: :ok

@spec get_audio_specific_config(String.t()) :: binary()
def get_audio_specific_config(mp4_path) do
{container, ""} = File.read!(mp4_path) |> Membrane.MP4.Container.parse!()

container[:moov].children
|> Keyword.get_values(:trak)
|> Enum.map(& &1.children[:mdia].children[:minf].children[:stbl].children[:stsd])
|> Enum.find_value(fn
%{children: [{:mp4a, mp4a_box}]} ->
mp4a_box.children[:esds].fields.elementary_stream_descriptor

_other ->
false
end)
|> Membrane.AAC.Parser.Esds.parse_esds()
|> Membrane.AAC.Parser.AudioSpecificConfig.generate_audio_specific_config()
end
end
93 changes: 60 additions & 33 deletions lib/membrane/simple_rtsp_server/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,18 @@ defmodule Membrane.SimpleRTSPServer.Pipeline do
@impl true
def handle_child_notification({:new_tracks, tracks}, :mp4_demuxer, _ctx, state) do
spec =
Enum.map(tracks, fn
{id, %Membrane.AAC{}} ->
get_child(:mp4_demuxer)
|> via_out(Pad.ref(:output, id))
|> child(Membrane.Debug.Sink)
[child(:rtp_session_bin, Membrane.RTP.SessionBin)] ++
Enum.map(tracks, fn
{id, %Membrane.AAC{}} ->
get_child(:mp4_demuxer)
|> via_out(Pad.ref(:output, id))
|> build_track(:audio, state.media_config)

{id, %Membrane.H264{}} ->
get_child(:mp4_demuxer)
|> via_out(Pad.ref(:output, id))
|> child(:parser, %Membrane.H264.Parser{
output_alignment: :nalu,
repeat_parameter_sets: true,
skip_until_keyframe: true,
output_stream_structure: :annexb
})
|> via_in(Pad.ref(:input, state.ssrc),
options: [payloader: Membrane.RTP.H264.Payloader]
)
|> child(:rtp, Membrane.RTP.SessionBin)
|> via_out(Pad.ref(:rtp_output, state.ssrc),
options: [
payload_type: state.pt,
clock_rate: state.clock_rate
]
)
|> child(:realtimer, Membrane.Realtimer)
|> child(:udp_sink, %Membrane.UDP.Sink{
destination_address: state.client_ip,
destination_port_no: state.client_port,
local_socket: state.server_rtp_socket
})
end)
{id, %Membrane.H264{}} ->
get_child(:mp4_demuxer)
|> via_out(Pad.ref(:output, id))
|> build_track(:video, state.media_config)
end)

{[spec: spec], state}
end
Expand All @@ -65,8 +45,7 @@ defmodule Membrane.SimpleRTSPServer.Pipeline do
end

@impl true
def handle_element_end_of_stream(:udp_sink, :input, _ctx, state) do
Process.sleep(50)
def handle_element_end_of_stream({:udp_sink, :video}, :input, _ctx, state) do
:gen_tcp.close(state.socket)
{[terminate: :normal], state}
end
Expand All @@ -75,4 +54,52 @@ defmodule Membrane.SimpleRTSPServer.Pipeline do
def handle_element_end_of_stream(_child, _pad, _ctx, state) do
{[], state}
end

defp build_track(builder, :audio, %{audio: config}) do
builder
|> child(:aac_parser, %Membrane.AAC.Parser{
out_encapsulation: :none,
output_config: :audio_specific_config
})
|> via_in(Pad.ref(:input, config.ssrc),
options: [payloader: %Membrane.RTP.AAC.Payloader{frames_per_packet: 1, mode: :hbr}]
)
|> build_tail(:audio, config)
end

defp build_track(builder, :video, %{video: config}) do
builder
|> child(:h264_parser, %Membrane.H264.Parser{
output_alignment: :nalu,
repeat_parameter_sets: true,
skip_until_keyframe: true,
output_stream_structure: :annexb
})
|> via_in(Pad.ref(:input, config.ssrc),
options: [payloader: Membrane.RTP.H264.Payloader]
)
|> build_tail(:video, config)
end

defp build_track(builder, _type, _media_config) do
builder
|> child(Membrane.Debug.Sink)
end

defp build_tail(builder, type, config) do
builder
|> get_child(:rtp_session_bin)
|> via_out(Pad.ref(:rtp_output, config.ssrc),
options: [
payload_type: config.pt,
clock_rate: config.clock_rate
]
)
|> child({:realtimer, type}, Membrane.Realtimer)
|> child({:udp_sink, type}, %Membrane.UDP.Sink{
destination_address: config.client_address,
destination_port_no: config.client_port,
local_socket: config.rtp_socket
})
end
end
7 changes: 4 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ defmodule Membrane.SimpleRTSPServer.Mixfile do
defp deps do
[
{:membrane_core, "~> 1.0"},
{:membrane_rtsp,
github: "membraneframework/membrane_rtsp", branch: "server-ports-optional"},
# {:membrane_rtsp, "~> 0.9.0"},
{:membrane_rtsp_plugin, "~> 0.5.0"},
{:membrane_rtp_plugin, "~> 0.29.0"},
{:membrane_rtp_h264_plugin, "~> 0.19.0"},
{:membrane_rtp_aac_plugin, "~> 0.9.0"},
{:membrane_file_plugin, "~> 0.17.0"},
{:membrane_mp4_plugin, "~> 0.35.0"},
{:membrane_h26x_plugin, "~> 0.10.0"},
{:membrane_aac_plugin, "~> 0.19.0", override: true},
{:ex_sdp, "~> 1.1"},
{:membrane_udp_plugin, "~> 0.14.0"},
{:membrane_realtimer_plugin, "~> 0.9.0"},
{:boombox, github: "membraneframework/boombox", branch: "fix-rtsp-track", only: :test},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
{:credo, ">= 0.0.0", only: :dev, runtime: false}
Expand Down
Loading