Skip to content

Commit

Permalink
Create server (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Noarkhh authored Nov 4, 2024
1 parent a1bcd3e commit 39b0621
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 81 deletions.
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

0 comments on commit 39b0621

Please sign in to comment.