├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── License ├── README.md ├── apps ├── amf0 │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ └── amf0.ex │ ├── mix.exs │ └── test │ │ ├── amf0 │ │ ├── deserializer_test.exs │ │ └── serializer_test.exs │ │ ├── amf0_test.exs │ │ └── test_helper.exs ├── amf3 │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── amf3.ex │ │ └── amf3 │ │ │ └── deserializer.ex │ ├── mix.exs │ └── test │ │ ├── amf3_deserialization_test.exs │ │ ├── amf3_test.exs │ │ └── test_helper.exs ├── flv │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── flv.ex │ │ └── flv │ │ │ ├── audio_data.ex │ │ │ └── video_data.ex │ ├── mix.exs │ └── test │ │ ├── flv │ │ ├── audio_data_test.exs │ │ └── video_data_test.exs │ │ ├── flv_test.exs │ │ └── test_helper.exs ├── gen_rtmp_client │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── gen_rtmp_client.ex │ │ └── gen_rtmp_client │ │ │ └── connection_info.ex │ ├── mix.exs │ └── test │ │ └── test_helper.exs ├── gen_rtmp_server │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── gen_rtmp_server.ex │ │ └── gen_rtmp_server │ │ │ ├── audio_video_data.ex │ │ │ ├── meta_data.ex │ │ │ ├── protocol.ex │ │ │ └── rtmp_options.ex │ ├── mix.exs │ └── test │ │ ├── gen_rtmp_server_test.exs │ │ └── test_helper.exs ├── rtmp │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── rtmp.ex │ │ └── rtmp │ │ │ ├── client_session │ │ │ ├── configuration.ex │ │ │ ├── events.ex │ │ │ └── handler.ex │ │ │ ├── handshake.ex │ │ │ ├── handshake │ │ │ ├── digest_handshake_format.ex │ │ │ ├── handshake_result.ex │ │ │ ├── old_handshake_format.ex │ │ │ ├── parse_result.ex │ │ │ └── result.ex │ │ │ ├── protocol │ │ │ ├── chunk_io.ex │ │ │ ├── detailed_message.ex │ │ │ ├── handler.ex │ │ │ ├── messages │ │ │ │ ├── abort.ex │ │ │ │ ├── acknowledgement.ex │ │ │ │ ├── amf0_command.ex │ │ │ │ ├── amf0_data.ex │ │ │ │ ├── audio_data.ex │ │ │ │ ├── set_chunk_size.ex │ │ │ │ ├── set_peer_bandwidth.ex │ │ │ │ ├── user_control.ex │ │ │ │ ├── video_data.ex │ │ │ │ └── window_acknowledgement_size.ex │ │ │ ├── raw_message.ex │ │ │ └── rtmp_time.ex │ │ │ ├── server_session │ │ │ ├── configuration.ex │ │ │ ├── events.ex │ │ │ └── handler.ex │ │ │ └── stream_metadata.ex │ ├── mix.exs │ └── test │ │ ├── client_session │ │ └── handler_test.exs │ │ ├── handshake │ │ ├── digest_handshake_format_test.exs │ │ └── general_handshake_test.exs │ │ ├── protocol │ │ ├── chunk_io_test.exs │ │ ├── handler_test.exs │ │ ├── raw_message_test.exs │ │ └── rtmp_time_test.exs │ │ ├── server_session │ │ └── handler_test.exs │ │ └── test_helper.exs ├── rtmp_reader_cli │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ └── rtmp_reader_cli.ex │ ├── mix.exs │ └── test │ │ ├── rtmp_reader_cli_test.exs │ │ └── test_helper.exs ├── simple_rtmp_player │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── simple_rtmp_player.ex │ │ └── simple_rtmp_player │ │ │ └── client.ex │ ├── mix.exs │ └── test │ │ ├── simple_rtmp_player_test.exs │ │ └── test_helper.exs ├── simple_rtmp_proxy │ ├── .gitignore │ ├── README.md │ ├── config │ │ └── config.exs │ ├── lib │ │ ├── simple_rtmp_proxy.ex │ │ └── simple_rtmp_proxy │ │ │ ├── client.ex │ │ │ └── server_worker.ex │ ├── mix.exs │ └── test │ │ ├── simple_rtmp_proxy_test.exs │ │ └── test_helper.exs └── simple_rtmp_server │ ├── .gitignore │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── simple_rtmp_server.ex │ └── simple_rtmp_server │ │ └── worker.ex │ ├── mix.exs │ ├── test │ ├── simple_rtmp_server_test.exs │ └── test_helper.exs │ └── ttb_last_config ├── config └── config.exs ├── mix.exs └── mix.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | dumps/ 7 | *.escript 8 | doc/ 9 | 10 | #Intellij 11 | **/.idea/ 12 | *.iml 13 | 14 | *.lock 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2 4 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "mix", 6 | "isShellCommand": true, 7 | "args": [], 8 | "showOutput": "always", 9 | "tasks": [ 10 | { 11 | "taskName": "compile", 12 | "args": [], 13 | "isBuildCommand": true, 14 | "echoCommand": true 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matthew Shapiro 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Media Libs 2 | 3 | The Elixir Media Libs is a collection of libraries and applications written in that that revolve around working with media. 4 | 5 | This project currently contains the following systems: 6 | * **amf0** - Library providing functionality for serializing and deserializing values with the AMF0 data format. 7 | * **amf3** - **INCOMPLETE** Library providing functionality for serializing and deserializing values with the AMF3 data format. 8 | * **flv** - **INCOMPLETE** Library providing functionality for reading FLV media files and streams 9 | * **rtmp** - Library providing functionality for handling RTMP handshakes, protocol (de)serialization, and low level/expandable server handling (with client handling coming soon) 10 | * **gen_rtmp_server** - A generic behaviour for building your own custom RTMP server 11 | * **simple_rtmp_server** - An example RTMP server built upon the `gen_rtmp_server` library, for testing and reference purposes. 12 | * **rtmp_reader_cli** - A command line application for reading raw RTMP chunk byte streams from a file. Mostly utilized to debug input and output dumps generated from the raw I/O logging mechanisms of a `rtmp_session`. -------------------------------------------------------------------------------- /apps/amf0/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /apps/amf0/README.md: -------------------------------------------------------------------------------- 1 | # Amf0 2 | 3 | Provides functions to serialize and deserialize data encoded in the AMF0 data format based on the official (Adobe Specification)[http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf0-file-format-specification.pdf]. 4 | 5 | This library so far implements basic types required for RTMP communication and thus currently supports: 6 | * numbers 7 | * booleans 8 | * UTF8 strings 9 | * Nulls 10 | * Arrays 11 | * Objects with properties 12 | 13 | ## Examples 14 | 15 | ``` 16 | iex> Amf0.deserialize(<<0::8, 532::float-64, 1::8, 1::8>>) 17 | {:ok, [532.0, true]} 18 | 19 | iex> Amf0.serialize("test") 20 | <<2::8, 4::16>> <> "test" 21 | 22 | iex> Amf0.serialize([532, true]) 23 | <<0::8, 532::float-64, 1::8, 1::8>> 24 | ``` -------------------------------------------------------------------------------- /apps/amf0/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :amf0, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:amf0, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/amf0/lib/amf0.ex: -------------------------------------------------------------------------------- 1 | defmodule Amf0 do 2 | @moduledoc """ 3 | Functions for serializing and deserializing AMF0 encoded data 4 | """ 5 | 6 | @doc """ 7 | Deserializes data from amf0 encoded binary 8 | 9 | ## Examples 10 | 11 | iex> Amf0.deserialize(<<0::8, 532::float-64, 1::8, 1::8>>) 12 | {:ok, [532.0, true]} 13 | 14 | """ 15 | @spec deserialize(<<>>) :: {:ok, [any()]} 16 | def deserialize(binary) when is_binary(binary) do 17 | do_deserialize(binary, []) 18 | end 19 | 20 | @doc """ 21 | Serializes the passed in values into AMF0 encoded binary 22 | 23 | ## Examples 24 | 25 | iex> Amf0.serialize("test") 26 | <<2::8, 4::16>> <> "test" 27 | 28 | iex> Amf0.serialize([532, true]) 29 | <<0::8, 532::float-64, 1::8, 1::8>> 30 | """ 31 | @spec serialize(any() | [any()]) :: <<>> 32 | def serialize(values) when is_list(values), do: do_serialize(values, <<>>) 33 | def serialize(value), do: do_serialize([value], <<>>) 34 | 35 | defp do_deserialize(<<>>, accumulator) do 36 | {:ok, Enum.reverse(accumulator)} 37 | end 38 | 39 | defp do_deserialize(<>, accumulator) do 40 | get_marker_type(marker) 41 | |> get_object(binary) 42 | |> do_deserialize(accumulator) 43 | end 44 | 45 | defp do_deserialize({object, binary}, accumulator) do 46 | # Transforms the get_object results for readability into the 47 | # proper arguments for do_deserialize 48 | do_deserialize(binary, [object | accumulator]) 49 | end 50 | 51 | defp get_marker_type(marker_number) do 52 | case marker_number do 53 | 0 -> :number 54 | 1 -> :boolean 55 | 2 -> :"utf8-1" # TODO: support other utf8 markers 56 | 3 -> :object 57 | 5 -> :null 58 | 6 -> :undefined 59 | 8 -> :emca_array 60 | end 61 | end 62 | 63 | defp get_object(:number, <>) do 64 | {number, rest} 65 | end 66 | 67 | defp get_object(:boolean, <>) do 68 | atom = if bool == 1, do: true, else: false 69 | {atom, rest} 70 | end 71 | 72 | defp get_object(:"utf8-1", <>) do 73 | <> = binary 74 | {string, rest} 75 | end 76 | 77 | defp get_object(:null, binary) do 78 | {nil, binary} 79 | end 80 | 81 | defp get_object(:undefined, binary) do 82 | {nil, binary} 83 | end 84 | 85 | defp get_object(:emca_array, binary) do 86 | <<_::32, binary::binary>> = binary 87 | 88 | {properties, rest} = get_object_properties(binary, %{}) 89 | {properties, rest} 90 | end 91 | 92 | defp get_object(:object, binary) do 93 | {properties, rest} = get_object_properties(binary, %{}) 94 | {properties, rest} 95 | end 96 | 97 | defp get_object_properties(<<0, 0, 9, binary::binary>>, properties) do 98 | {properties, binary} 99 | end 100 | 101 | defp get_object_properties(<>, properties) do 102 | <> = binary 103 | 104 | get_marker_type(type_marker) 105 | |> get_object(rest) 106 | |> form_object_property(name, properties) 107 | end 108 | 109 | defp form_object_property({object, binary}, property_name, properties) do 110 | get_object_properties(binary, Map.put(properties, property_name, object)) 111 | end 112 | 113 | defp do_serialize([], binary) do 114 | binary 115 | end 116 | 117 | defp do_serialize([value | rest], binary) when is_number(value) do 118 | do_serialize(rest, binary <> <<0::8, value::float-64>>) 119 | end 120 | 121 | defp do_serialize([value | rest], binary) when is_boolean(value) do 122 | bit = if value, do: 1, else: 0 123 | 124 | do_serialize(rest, binary <> <<1::8, bit::8>>) 125 | end 126 | 127 | defp do_serialize([value | rest], binary) when is_binary(value) do 128 | length = String.length(value) 129 | 130 | do_serialize(rest, binary <> <<2::8, length::16>> <> value) 131 | end 132 | 133 | defp do_serialize([nil | rest], binary) do 134 | do_serialize(rest, binary <> <<5::8>>) 135 | end 136 | 137 | defp do_serialize([properties | rest], binary) when is_map(properties) do 138 | serialized_properties = Enum.map(Map.keys(properties), fn(x) -> serialize_property(x, Map.get(properties, x)) end) 139 | |> Enum.reduce(fn(x, acc) -> acc <> x end) 140 | 141 | do_serialize(rest, binary <> <<3>> <> serialized_properties <> <<0, 0, 9>>) 142 | end 143 | 144 | defp serialize_property(name, value) do 145 | length = byte_size(name) 146 | binary = <> <> name 147 | 148 | do_serialize([value], binary) 149 | end 150 | 151 | end -------------------------------------------------------------------------------- /apps/amf0/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf0.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :amf0, 7 | version: "1.0.1", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.2", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | description: description(), 16 | package: package(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | def application do 22 | [applications: [:logger]] 23 | end 24 | 25 | defp deps do 26 | [{:ex_doc, "~> 0.14", only: :dev}] 27 | end 28 | 29 | defp package do 30 | [ 31 | name: :eml_amf0, 32 | maintainers: ["Matthew Shapiro"], 33 | licenses: ["MIT"], 34 | links: %{"GitHub" => "https://github.com/KallDrexx/elixir-media-libs/tree/master/apps/amf0"} 35 | ] 36 | end 37 | 38 | defp description do 39 | "Provides functions to serialize and deserialize data encoded in the AMF0 data format" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /apps/amf0/test/amf0/deserializer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf0.DeserializerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Can deserialize number" do 5 | binary = <<0::8, 532::float-64>> 6 | 7 | assert {:ok, [532.0]} = Amf0.deserialize(binary) 8 | end 9 | 10 | test "Can deserialize true boolean" do 11 | binary = <<1::8, 1::8>> 12 | 13 | assert {:ok, [true]} = Amf0.deserialize(binary) 14 | end 15 | 16 | test "Can deserialize false boolean" do 17 | binary = <<1::8, 0::8>> 18 | 19 | assert {:ok, [false]} = Amf0.deserialize(binary) 20 | end 21 | 22 | test "Can deserialize UTF8-1 string" do 23 | binary = <<2::8, 4::16>> <> "test" 24 | 25 | assert {:ok, ["test"]} = Amf0.deserialize(binary) 26 | end 27 | 28 | test "Can deserialize null" do 29 | binary = <<5::8>> 30 | 31 | assert {:ok, [nil]} = Amf0.deserialize(binary) 32 | end 33 | 34 | test "Can deserialize object" do 35 | binary = <<3::8, 4::16>> <> "test" <> <<2::8, 5::16>> <> "value" <> <<0, 0, 9>> 36 | 37 | assert {:ok, [%{"test" => "value"}]} = Amf0.deserialize(binary) 38 | end 39 | 40 | test "Can deserialize consecutive values" do 41 | binary = <<0::8, 532::float-64, 1::8, 1::8>> 42 | 43 | assert {:ok, [532.0, true]} = Amf0.deserialize(binary) 44 | end 45 | 46 | test "Can deserialize object with multiple properties (rtmp connect object)" do 47 | binary = <<0x03, 0x00, 0x03, 0x61, 0x70, 0x70, 0x02, 0x00, 0x04, 0x6c, 0x69, 0x76, 0x65, 0x00, 0x04, 0x74, 0x79, 0x70, 0x65, 0x02, 0x00, 0x0a, 0x6e, 0x6f, 0x6e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x00, 0x08, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x56, 0x65, 0x72, 0x02, 0x00, 0x1f, 0x46, 0x4d, 0x4c, 0x45, 0x2f, 0x33, 0x2e, 0x30, 0x20, 0x28, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x3b, 0x20, 0x46, 0x4d, 0x53, 0x63, 0x2f, 0x31, 0x2e, 0x30, 0x29, 0x00, 0x06, 0x73, 0x77, 0x66, 0x55, 0x72, 0x6c, 0x02, 0x00, 0x16, 0x72, 0x74, 0x6d, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x36, 0x39, 0x2e, 0x35, 0x35, 0x2e, 0x38, 0x2e, 0x34, 0x2f, 0x6c, 0x69, 0x76, 0x65, 0x00, 0x05, 0x74, 0x63, 0x55, 0x72, 0x6c, 0x02, 0x00, 0x16, 0x72, 0x74, 0x6d, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x36, 0x39, 0x2e, 0x35, 0x35, 0x2e, 0x38, 0x2e, 0x34, 0x2f, 0x6c, 0x69, 0x76, 0x65, 0x00, 0x00, 0x09>> 48 | 49 | assert {:ok, [%{ 50 | "app" => "live", 51 | "type" => "nonprivate", 52 | "flashVer" => "FMLE/3.0 (compatible; FMSc/1.0)", 53 | "swfUrl" => "rtmp://169.55.8.4/live", 54 | "tcUrl" => "rtmp://169.55.8.4/live" 55 | }]} = Amf0.deserialize(binary) 56 | end 57 | 58 | test "Can deserialize EMCA array" do 59 | binary = <<0x08, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 61 | 0x7a, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x77, 0x69, 0x64, 62 | 0x74, 0x68, 0x00, 0x40, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x68, 0x65, 0x69, 63 | 0x67, 0x68, 0x74, 0x00, 0x40, 0x86, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x76, 0x69, 64 | 0x64, 0x65, 0x6f, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x69, 0x64, 0x02, 0x00, 0x04, 0x61, 0x76, 0x63, 65 | 0x31, 0x00, 0x0d, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x72, 0x61, 0x74, 0x65, 66 | 0x00, 0x40, 0x8c, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x66, 0x72, 0x61, 0x6d, 0x65, 67 | 0x72, 0x61, 0x74, 0x65, 0x00, 0x40, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x61, 68 | 0x75, 0x64, 0x69, 0x6f, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x69, 0x64, 0x02, 0x00, 0x04, 0x6d, 0x70, 69 | 0x34, 0x61, 0x00, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x64, 0x61, 0x74, 0x61, 0x72, 0x61, 0x74, 70 | 0x65, 0x00, 0x40, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x75, 0x64, 0x69, 71 | 0x6f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x72, 0x61, 0x74, 0x65, 0x00, 0x40, 0xe5, 0x88, 0x80, 72 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 73 | 0x65, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x40, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 74 | 0x61, 0x75, 0x64, 0x69, 0x6f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x00, 0x40, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x73, 0x74, 0x65, 0x72, 0x65, 0x6f, 0x01, 0x01, 76 | 0x00, 0x07, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x02, 0x00, 0x29, 0x6f, 0x62, 0x73, 0x2d, 77 | 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x20, 0x28, 0x6c, 78 | 0x69, 0x62, 0x6f, 0x62, 0x73, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x30, 0x2e, 79 | 0x31, 0x34, 0x2e, 0x32, 0x29, 0x00, 0x00, 0x09>> 80 | 81 | assert {:ok, [%{ 82 | "duration" => 0, 83 | "fileSize" => 0, 84 | "width" => 1280, 85 | "height" => 720, 86 | "videocodecid" => "avc1", 87 | "videodatarate" => 900, 88 | "framerate" => 30, 89 | "audiocodecid" => "mp4a", 90 | "audiodatarate" => 160, 91 | "audiosamplerate" => 44100, 92 | "audiosamplesize" => 16, 93 | "audiochannels" => 2, 94 | "stereo" => true, 95 | "encoder" => "obs-output module (libobs version 0.14.2)" 96 | }]} == Amf0.deserialize(binary) 97 | end 98 | 99 | end -------------------------------------------------------------------------------- /apps/amf0/test/amf0/serializer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf0.SerializerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Can serialize number" do 5 | assert <<0::8, 332::float-64>> = Amf0.serialize(332) 6 | end 7 | 8 | test "Can serialize true boolean" do 9 | assert <<1::8, 1::8>> = Amf0.serialize(true) 10 | end 11 | 12 | test "Can serialize UTF8-1 string" do 13 | assert (<<2::8, 4::16>> <> "test") = Amf0.serialize("test") 14 | end 15 | 16 | test "Can serialize null value" do 17 | assert <<5::8>> = Amf0.serialize(nil) 18 | end 19 | 20 | test "Can serialize object" do 21 | assert (<<3::8, 4::16>> <> "test" <> <<2::8, 5::16>> <> "value" <> <<0, 0, 9>>) 22 | = Amf0.serialize(%{"test" => "value"}) 23 | end 24 | 25 | test "Can serialize multiple values" do 26 | assert (<<0::8, 532::float-64, 1::8, 1::8>>) = Amf0.serialize([532, true]) 27 | end 28 | 29 | test "Can serialize then deserialize complex object (rtmp connect)" do 30 | # Since we can't predict map ordering, we can't guarantee order of binary, 31 | # so just make sure that the complex object can be serialized then deserialized 32 | # back again. 33 | 34 | object = %{ 35 | "app" => "live", 36 | "type" => "nonprivate", 37 | "flashVer" => "FMLE/3.0 (compatible; FMSc/1.0)", 38 | "swfUrl" => "rtmp://169.55.8.4/live", 39 | "tcUrl" => "rtmp://169.55.8.4/live" 40 | } 41 | 42 | {:ok, [result]} = 43 | Amf0.serialize(object) 44 | |> Amf0.deserialize() 45 | 46 | assert object == result 47 | end 48 | end -------------------------------------------------------------------------------- /apps/amf0/test/amf0_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf0Test do 2 | use ExUnit.Case, async: true 3 | doctest Amf0 4 | end -------------------------------------------------------------------------------- /apps/amf0/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) -------------------------------------------------------------------------------- /apps/amf3/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/amf3/README.md: -------------------------------------------------------------------------------- 1 | # Amf3 2 | 3 | **WARNING:** This library is incomplete and should not be used. Initially created for AMF3 support with RTMP it was abandoned when it was realized that AMF3 RTMP clients send AMF0 encoded data inside of AMF3 messages. -------------------------------------------------------------------------------- /apps/amf3/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :amf3, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:amf3, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/amf3/lib/amf3.ex: -------------------------------------------------------------------------------- 1 | defmodule Amf3 do 2 | @moduledoc """ 3 | Functions to serialize and deserialize AMF3 encoded data. 4 | 5 | Based on the Adobe specification found at 6 | http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf-file-format-spec.pdf 7 | """ 8 | 9 | @spec deserialize(<<>>) :: [false | nil | true | number] 10 | def deserialize(binary) when is_binary(binary) do 11 | Amf3.Deserializer.deserialize(binary) 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /apps/amf3/lib/amf3/deserializer.ex: -------------------------------------------------------------------------------- 1 | defmodule Amf3.Deserializer do 2 | use Bitwise 3 | 4 | @spec deserialize(<<>>) :: [any] 5 | def deserialize(binary) do 6 | do_deserialize(binary, []) 7 | end 8 | 9 | defp do_deserialize(<<>>, accumulator) do 10 | Enum.reverse(accumulator) 11 | end 12 | 13 | defp do_deserialize(<<0x00, rest::binary>>, accumulator) do 14 | do_deserialize(rest, [nil | accumulator]) 15 | end 16 | 17 | defp do_deserialize(<<0x01, rest::binary>>, accumulator) do 18 | do_deserialize(rest, [nil | accumulator]) 19 | end 20 | 21 | defp do_deserialize(<<0x02, rest::binary>>, accumulator) do 22 | do_deserialize(rest, [false | accumulator]) 23 | end 24 | 25 | defp do_deserialize(<<0x03, rest::binary>>, accumulator) do 26 | do_deserialize(rest, [true | accumulator]) 27 | end 28 | 29 | defp do_deserialize(<<0x04, rest::binary>>, accumulator) do 30 | {value, rest} = get_u29(rest) 31 | 32 | do_deserialize(rest, [u29_to_i29(value) | accumulator]) 33 | end 34 | 35 | defp do_deserialize(<<0x05, value::float-64, rest::binary>>, accumulator) do 36 | do_deserialize(rest, [value | accumulator]) 37 | end 38 | 39 | defp get_u29(<<0::1, byte::7, rest::binary>>) do 40 | {byte, rest} 41 | end 42 | 43 | defp get_u29(<<1::1, b1::7, 0::1, b2::7, rest::binary>>) do 44 | value = Bitwise.bsl(b1, 7) 45 | |> Bitwise.bor(b2) 46 | 47 | {value, rest} 48 | end 49 | 50 | defp get_u29(<<1::1, b1::7, 1::1, b2::7, 0::1, b3::7, rest::binary>>) do 51 | value = Bitwise.bsl(b1, 14) 52 | |> Bitwise.bor(Bitwise.bsl(b2, 7)) 53 | |> Bitwise.bor(b3) 54 | 55 | {value, rest} 56 | end 57 | 58 | defp get_u29(<<1::1, b1::7, 1::1, b2::7, 1::1, b3::7, b4, rest::binary>>) do 59 | value = Bitwise.bsl(b1,22) 60 | |> Bitwise.bor(Bitwise.bsl(b2, 15)) 61 | |> Bitwise.bor(Bitwise.bsl(b3, 8)) 62 | |> Bitwise.bor(b4) 63 | 64 | {value, rest} 65 | end 66 | 67 | # If the u29's first bit is 1 (> :math.pow(2,28), then subtract the value from :math.pow(2,39) 68 | defp u29_to_i29(value) when value > 268435455, do: value - 536870912 69 | defp u29_to_i29(value), do: value 70 | end -------------------------------------------------------------------------------- /apps/amf3/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf3.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :amf3, 6 | version: "0.1.0", 7 | build_path: "../../_build", 8 | config_path: "../../config/config.exs", 9 | deps_path: "../../deps", 10 | lockfile: "../../mix.lock", 11 | elixir: "~> 1.3", 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps()] 15 | end 16 | 17 | # Configuration for the OTP application 18 | # 19 | # Type "mix help compile.app" for more information 20 | def application do 21 | [applications: [:logger]] 22 | end 23 | 24 | # Dependencies can be Hex packages: 25 | # 26 | # {:mydep, "~> 0.3.0"} 27 | # 28 | # Or git/path repositories: 29 | # 30 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 31 | # 32 | # To depend on another app inside the umbrella: 33 | # 34 | # {:myapp, in_umbrella: true} 35 | # 36 | # Type "mix help deps" for more examples and options 37 | defp deps do 38 | [] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /apps/amf3/test/amf3_deserialization_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf3DeserializationTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Undefined marker deserializes to nil" do 5 | assert [nil] == Amf3.deserialize(<<0x00>>) 6 | end 7 | 8 | test "Null marker deserializes to nil" do 9 | assert [nil] == Amf3.deserialize(<<0x01>>) 10 | end 11 | 12 | test "False marker deserializes to false" do 13 | assert [false] == Amf3.deserialize(<<0x02>>) 14 | end 15 | 16 | test "True marker deserializes to true" do 17 | assert [true] == Amf3.deserialize(<<0x03>>) 18 | end 19 | 20 | test "Integer marker with value deserializes to number" do 21 | assert [127] == Amf3.deserialize(<<0x04, 0x7f>>) 22 | assert [127] == Amf3.deserialize(<<0x04, 0x80, 0x7f>>) 23 | assert [16383] == Amf3.deserialize(<<0x04, 0x80, 0xff, 0x7f>>) 24 | assert [4194303] == Amf3.deserialize(<<0x04, 0x80, 0xff, 0xff, 0xff>>) 25 | assert [-26] == Amf3.deserialize(<<0x04, 0xff, 0xff, 0xff, 0xe6>>) 26 | end 27 | 28 | test "Double marker with value deserializes to number" do 29 | assert [532.5] == Amf3.deserialize(<<0x05, 532.5::float-64>>) 30 | end 31 | end -------------------------------------------------------------------------------- /apps/amf3/test/amf3_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Amf3Test do 2 | use ExUnit.Case 3 | doctest Amf3 4 | 5 | 6 | end 7 | -------------------------------------------------------------------------------- /apps/amf3/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/flv/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/flv/README.md: -------------------------------------------------------------------------------- 1 | # Flv 2 | 3 | Functionality for parsing FLV encoded media streams. This library is a work in progress and is not very usable at the moment. -------------------------------------------------------------------------------- /apps/flv/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :flv, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:flv, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/flv/lib/flv.ex: -------------------------------------------------------------------------------- 1 | defmodule Flv do 2 | @moduledoc """ 3 | Contains functions for working with FLV streams 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /apps/flv/lib/flv/audio_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Flv.AudioData do 2 | @moduledoc "Represents a packet of audio data." 3 | 4 | @type sound_format :: :pcm_platform_endian | :adpcm | :mp3 | :pcm_little_endian | 5 | :nelly_16khz | :nelly_8khz | :nelly | :g711_alaw | :g711_mulaw | 6 | :reserved | :aac | :speex | :mp3_8khz | :device_specific 7 | 8 | @type sample_rate :: 5 | 11 | 22 | 44 9 | @type sample_size :: 8 | 16 10 | @type channel_type :: :mono | :stereo 11 | @type aac_packet_type :: :sequence_header | :raw_data | :not_aac 12 | 13 | @type t :: %__MODULE__{ 14 | format: sound_format, 15 | sample_rate_in_khz: sample_rate, 16 | sample_size_in_bits: sample_size, 17 | channel_type: channel_type, 18 | aac_packet_type: aac_packet_type, 19 | data: binary 20 | } 21 | 22 | defstruct format: nil, 23 | sample_rate_in_khz: nil, 24 | sample_size_in_bits: nil, 25 | channel_type: nil, 26 | aac_packet_type: :not_aac, 27 | data: <<>> 28 | 29 | @spec parse(binary) :: {:ok, __MODULE__.t} | :error 30 | @doc "Parses the provided binary into an flv video tag" 31 | def parse(binary) when is_binary(binary) do 32 | do_parse(binary) 33 | end 34 | 35 | defp do_parse(<>) do 36 | format = get_format(format_id) 37 | rate = get_rate(rate_id) 38 | size = get_size(size_id) 39 | type = get_channel_type(type_id) 40 | 41 | case format != :error && rate != :error && size != :error && type != :error do 42 | true -> 43 | audio = %__MODULE__{ 44 | format: format, 45 | sample_rate_in_khz: rate, 46 | sample_size_in_bits: size, 47 | channel_type: type, 48 | } 49 | 50 | {:ok, apply_data(rest, audio)} 51 | 52 | false -> 53 | :error 54 | end 55 | 56 | end 57 | 58 | defp do_parse(_) do 59 | :error 60 | end 61 | 62 | defp get_format(0), do: :pcm_platform_endian 63 | defp get_format(1), do: :adpcm 64 | defp get_format(2), do: :mp3 65 | defp get_format(3), do: :pcm_little_endian 66 | defp get_format(4), do: :nelly_16khz 67 | defp get_format(5), do: :nelly_8khz 68 | defp get_format(6), do: :nelly 69 | defp get_format(7), do: :g711_alaw 70 | defp get_format(8), do: :g711_mulaw 71 | defp get_format(9), do: :reserved 72 | defp get_format(10), do: :aac 73 | defp get_format(11), do: :speex 74 | defp get_format(14), do: :mp3_8khz 75 | defp get_format(15), do: :device_specific 76 | defp get_format(_), do: :error 77 | 78 | defp get_rate(0), do: 5 # should be 5.5, but for some reason the typesepec is not allowing decimals 79 | defp get_rate(1), do: 11 80 | defp get_rate(2), do: 22 81 | defp get_rate(3), do: 44 82 | 83 | defp get_size(0), do: 8 84 | defp get_size(1), do: 16 85 | 86 | defp get_channel_type(0), do: :mono 87 | defp get_channel_type(1), do: :stereo 88 | 89 | defp apply_data(<<0x00, rest::binary>>, audio_data = %__MODULE__{format: :aac}) do 90 | %{audio_data | 91 | aac_packet_type: :sequence_header, 92 | data: rest 93 | } 94 | end 95 | 96 | defp apply_data(<<0x01, rest::binary>>, audio_data = %__MODULE__{format: :aac}) do 97 | %{audio_data | 98 | aac_packet_type: :raw_data, 99 | data: rest 100 | } 101 | end 102 | 103 | defp apply_data(binary, audio_data) do 104 | %{audio_data | data: binary} 105 | end 106 | end -------------------------------------------------------------------------------- /apps/flv/lib/flv/video_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Flv.VideoData do 2 | @moduledoc "Represents a packet of video data" 3 | 4 | @type frametype :: :keyframe | :interframe 5 | @type codec_id :: :avc 6 | @type avc_packet_type :: :sequence_header | :nalu 7 | 8 | @type t :: %__MODULE__{ 9 | frame_type: frametype, 10 | codec_id: codec_id, 11 | avc_packet_type: avc_packet_type, 12 | composition_time: non_neg_integer, 13 | data: <<>> 14 | } 15 | 16 | defstruct frame_type: nil, 17 | codec_id: nil, 18 | avc_packet_type: nil, 19 | composition_time: nil, 20 | data: <<>> 21 | 22 | 23 | @spec parse(<<>>) :: {:ok, __MODULE__.t} | :error 24 | @doc "Parses the video details from the supplied video packet" 25 | def parse(binary) do 26 | do_parse_video(binary) 27 | end 28 | 29 | defp do_parse_video(<>) do 30 | {:ok, %Flv.VideoData{ 31 | frame_type: video_frame_type(frame_type_id), 32 | codec_id: :avc, 33 | avc_packet_type: :sequence_header, 34 | composition_time: 0, 35 | data: rest 36 | }} 37 | end 38 | 39 | defp do_parse_video(<>) do 40 | {:ok, %Flv.VideoData{ 41 | frame_type: video_frame_type(frame_type_id), 42 | codec_id: :avc, 43 | avc_packet_type: :nalu, 44 | composition_time: time, 45 | data: rest 46 | }} 47 | end 48 | 49 | defp do_parse_video(_) do 50 | :error 51 | end 52 | 53 | defp video_frame_type(1), do: :keyframe 54 | defp video_frame_type(2), do: :interframe 55 | defp video_frame_type(_), do: :unknown 56 | 57 | end -------------------------------------------------------------------------------- /apps/flv/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Flv.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :flv, 6 | version: "0.1.0", 7 | build_path: "../../_build", 8 | config_path: "../../config/config.exs", 9 | deps_path: "../../deps", 10 | lockfile: "../../mix.lock", 11 | elixir: "~> 1.3", 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps()] 15 | end 16 | 17 | # Configuration for the OTP application 18 | # 19 | # Type "mix help compile.app" for more information 20 | def application do 21 | [applications: [:logger]] 22 | end 23 | 24 | # Dependencies can be Hex packages: 25 | # 26 | # {:mydep, "~> 0.3.0"} 27 | # 28 | # Or git/path repositories: 29 | # 30 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 31 | # 32 | # To depend on another app inside the umbrella: 33 | # 34 | # {:myapp, in_umbrella: true} 35 | # 36 | # Type "mix help deps" for more examples and options 37 | defp deps do 38 | [] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /apps/flv/test/flv/audio_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Flv.AudioDataTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Can parse audio aac sequence header" do 5 | binary = <<0xaf, 0x00, 0x12, 0x10, 0x56, 0xe2>> 6 | 7 | assert {:ok, %Flv.AudioData{ 8 | format: :aac, 9 | sample_rate_in_khz: 44, 10 | sample_size_in_bits: 16, 11 | channel_type: :stereo, 12 | aac_packet_type: :sequence_header, 13 | data: <<0x12, 0x10, 0x56, 0xe2>> 14 | }} = Flv.AudioData.parse(binary) 15 | end 16 | 17 | test "Can parse raw aac packet" do 18 | binary = Base.decode16!("AF01270C54FA06C301B0C068301A0B0A86E1A0C05830260D0604C180B0604C281210828120A04828113FB187B1640590B2C868B21F0083FFB139156216BD06F65473FA4F6195B38717ED536BF3D1FF214F1E111D2C20ABCE794C77125E98675A85D62205EEFB3DDF1EF306A738F25193B4D3EB48FCF866C122521608CF77D1A6B956F316FCBD429979FCFE8E5A27F47C485E6CD3BF65FEF114DB9AB016C7CBFE877DF8E4CE877E9941704C18AF9137B68A2A45182615E46C1B5B09C651B35DF643FD3A864A5F8A1ACE7E8E56FCC19242CA9CE528E22FBD3ACEDA1188203BCE21DB30129846779DFD5F56FCD9746CE79720377BEE3FC12F47BEEDF9EE8BEBCE8F24E97C5A559DE3FB459D9C30508E63CC40CFD957DCCCFF4007414370EE5CC4CA63668EA09EB98898731E3741E6C6AAC25254539461ADBB45A2026F548A16CCA5A9488BFE9AEE5D63952B656961A484678834280A06024180909848360A15422140904C221308B0022103D90E88428B2CB210859C061E89374ACF13ECF96FA2F7D37B72F4BED5263D227F22FB496E7EAF55FA9EA1FD85BD0F249F4427FB5B2E5C6F8DFF2CDE4B49B3857F366728AA63534F5E5D7D30E4B993EDCD12D2F887DD3187D898FDBB73891971D212E952060081724B48002C05A0B0016018818823C0") 19 | expected_data = Base.decode16!("270C54FA06C301B0C068301A0B0A86E1A0C05830260D0604C180B0604C281210828120A04828113FB187B1640590B2C868B21F0083FFB139156216BD06F65473FA4F6195B38717ED536BF3D1FF214F1E111D2C20ABCE794C77125E98675A85D62205EEFB3DDF1EF306A738F25193B4D3EB48FCF866C122521608CF77D1A6B956F316FCBD429979FCFE8E5A27F47C485E6CD3BF65FEF114DB9AB016C7CBFE877DF8E4CE877E9941704C18AF9137B68A2A45182615E46C1B5B09C651B35DF643FD3A864A5F8A1ACE7E8E56FCC19242CA9CE528E22FBD3ACEDA1188203BCE21DB30129846779DFD5F56FCD9746CE79720377BEE3FC12F47BEEDF9EE8BEBCE8F24E97C5A559DE3FB459D9C30508E63CC40CFD957DCCCFF4007414370EE5CC4CA63668EA09EB98898731E3741E6C6AAC25254539461ADBB45A2026F548A16CCA5A9488BFE9AEE5D63952B656961A484678834280A06024180909848360A15422140904C221308B0022103D90E88428B2CB210859C061E89374ACF13ECF96FA2F7D37B72F4BED5263D227F22FB496E7EAF55FA9EA1FD85BD0F249F4427FB5B2E5C6F8DFF2CDE4B49B3857F366728AA63534F5E5D7D30E4B993EDCD12D2F887DD3187D898FDBB73891971D212E952060081724B48002C05A0B0016018818823C0") 20 | 21 | assert {:ok, %Flv.AudioData{ 22 | format: :aac, 23 | sample_rate_in_khz: 44, 24 | sample_size_in_bits: 16, 25 | channel_type: :stereo, 26 | aac_packet_type: :raw_data, 27 | data: ^expected_data 28 | }} = Flv.AudioData.parse(binary) 29 | end 30 | end -------------------------------------------------------------------------------- /apps/flv/test/flv/video_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Flv.VideoDataTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Can parse avc keyframe with sequence header packet type" do 5 | binary = Base.decode16!("17000000000164001FFFE1001B6764001FACD9405005BA6A021A0280000003008000001E478C18CB01000468EFBCB0") 6 | expected_data = Base.decode16!("0000000164001FFFE1001B6764001FACD9405005BA6A021A0280000003008000001E478C18CB01000468EFBCB0") 7 | 8 | assert {:ok, %Flv.VideoData{ 9 | frame_type: :keyframe, 10 | codec_id: :avc, 11 | avc_packet_type: :sequence_header, 12 | composition_time: 0, 13 | data: ^expected_data 14 | }} = Flv.VideoData.parse(binary) 15 | end 16 | 17 | test "Can parse avc keyframe with nalu packet type" do 18 | binary = Base.decode16!("1701000042000002F30605FFFFEFDC45E9BDE6D948B7962CD820D923EEEF78323634202D20636F7265203134362072323533382031323133393663202D20482E3236342F4D5045472D342041564320636F646563202D20436F70796C6566742032303033") 19 | expected_data = Base.decode16!("000002F30605FFFFEFDC45E9BDE6D948B7962CD820D923EEEF78323634202D20636F7265203134362072323533382031323133393663202D20482E3236342F4D5045472D342041564320636F646563202D20436F70796C6566742032303033") 20 | 21 | assert {:ok, %Flv.VideoData{ 22 | frame_type: :keyframe, 23 | codec_id: :avc, 24 | avc_packet_type: :nalu, 25 | composition_time: 66, 26 | data: ^expected_data 27 | }} = Flv.VideoData.parse(binary) 28 | end 29 | 30 | test "Can parse avc interframe with nalu packet type" do 31 | binary = Base.decode16!("270100004300000366419A211888FFDAC9C56643D3F25D669E7653") 32 | expected_data = Base.decode16!("00000366419A211888FFDAC9C56643D3F25D669E7653") 33 | 34 | assert {:ok, %Flv.VideoData{ 35 | frame_type: :interframe, 36 | codec_id: :avc, 37 | avc_packet_type: :nalu, 38 | composition_time: 67, 39 | data: ^expected_data 40 | }} = Flv.VideoData.parse(binary) 41 | end 42 | 43 | test "Error when invalid video packet" do 44 | binary = Base.decode16!("FF4300000366419A211888FFDAC9C56643D3F25D669E") 45 | 46 | assert :error = Flv.VideoData.parse(binary) 47 | end 48 | end -------------------------------------------------------------------------------- /apps/flv/test/flv_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FlvTest do 2 | use ExUnit.Case 3 | doctest Flv 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/flv/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/gen_rtmp_client/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /apps/gen_rtmp_client/README.md: -------------------------------------------------------------------------------- 1 | # GenRtmpClient 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `gen_rtmp_client` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [{:gen_rtmp_client, "~> 0.1.0"}] 13 | end 14 | ``` 15 | 16 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 17 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 18 | be found at [https://hexdocs.pm/gen_rtmp_client](https://hexdocs.pm/gen_rtmp_client). 19 | 20 | -------------------------------------------------------------------------------- /apps/gen_rtmp_client/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :gen_rtmp_client, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:gen_rtmp_client, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/gen_rtmp_client/lib/gen_rtmp_client/connection_info.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpClient.ConnectionInfo do 2 | @type t :: %__MODULE__{ 3 | host: String.t, 4 | port: pos_integer, 5 | app_name: Rtmp.app_name, 6 | connection_id: String.t 7 | } 8 | 9 | defstruct host: nil, 10 | port: nil, 11 | app_name: nil, 12 | connection_id: nil 13 | end -------------------------------------------------------------------------------- /apps/gen_rtmp_client/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpClient.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gen_rtmp_client, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | package: package(), 16 | description: "Behaviour to make it easy to create custom RTMP clients", 17 | deps: deps() 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:ex_doc, "~> 0.14", only: [:dev, :publish, :umbrella]} | 30 | get_umbrella_dependencies(Mix.env) 31 | ] 32 | end 33 | 34 | defp get_umbrella_dependencies(:umbrella) do 35 | [ 36 | {:rtmp, in_umbrella: true}, 37 | ] 38 | end 39 | 40 | defp get_umbrella_dependencies(_) do 41 | [ 42 | {:rtmp, "~> 0.2.0", hex: :eml_rtmp}, 43 | ] 44 | end 45 | 46 | defp package do 47 | [ 48 | name: :eml_gen_rtmp_client, 49 | maintainers: ["Matthew Shapiro"], 50 | licenses: ["MIT"], 51 | links: %{"GitHub" => "https://github.com/KallDrexx/elixir-media-libs/tree/master/apps/gen_rtmp_client"} 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /apps/gen_rtmp_client/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/README.md: -------------------------------------------------------------------------------- 1 | # GenRtmpServer 2 | 3 | `GenRtmpServer` is a behaviour that allows developers to easily implement custom RTMP servers without worrying about the underlying protocol or internal RTMP workflows (such as chunk sizes). 4 | 5 | It will trigger functions in modules that implement the behaviour when any events occur that may need application specific workflows, such as if a connection should be accepted or rejected, if a connection should be allowed to publish or play video on a specific stream key, or even when audio and video data is received. 6 | 7 | It also contains functions for sending RTMP messages back to clients as an application determines it is needed. -------------------------------------------------------------------------------- /apps/gen_rtmp_server/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :gen_rtmp_server, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:gen_rtmp_server, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/lib/gen_rtmp_server.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServer do 2 | @moduledoc """ 3 | A behaviour module for implementing an RTMP server. 4 | 5 | A GenRtmpServer abstracts out the the handling of RTMP connection handling 6 | and data so that modules that implement this behaviour can focus on 7 | the business logic of the actual RTMP events that are received and 8 | should be sent. 9 | 10 | Each client that connects is placed in it's own process. 11 | """ 12 | 13 | require Logger 14 | 15 | @type session_id :: String.t 16 | @type client_ip :: String.t 17 | @type adopter_state :: any 18 | @type command :: :ignore | :disconnect 19 | @type request_result :: :accepted | {:rejected, command, String.t} 20 | @type outbound_data :: GenRtmpServer.AudioVideoData.t | GenRtmpServer.MetaData.t 21 | @type stream_id :: non_neg_integer 22 | @type forced_timestamp :: non_neg_integer | nil 23 | @type adopter_arguments :: [...] 24 | 25 | @doc "Called when a new RTMP client connects" 26 | @callback init(session_id, client_ip, adopter_arguments) :: {:ok, adopter_state} 27 | 28 | @doc "Called when the client is requesting a connection to the specified application name" 29 | @callback connection_requested(Rtmp.ServerSession.Events.ConnectionRequested.t, adopter_state) 30 | :: {request_result, adopter_state} 31 | 32 | @doc """ 33 | Called when a client wants to publish a stream to the specified application name 34 | and stream key combination 35 | """ 36 | @callback publish_requested(Rtmp.ServerSession.Events.PublishStreamRequested.t, adopter_state) 37 | :: {request_result, adopter_state} 38 | 39 | @doc """ 40 | Called when the client is no longer publishing to the specified application name 41 | and stream key 42 | """ 43 | @callback publish_finished(Rtmp.ServerSession.Events.PublishingFinished.t, adopter_state) 44 | :: {:ok, adopter_state} 45 | 46 | @doc """ 47 | Called when the client is wanting to play a stream from the specified application 48 | name and stream key combination 49 | """ 50 | @callback play_requested(Rtmp.ServerSession.Events.PlayStreamRequested.t, adopter_state) 51 | :: {request_result, adopter_state} 52 | 53 | @doc """ 54 | Called when the client no longer wants to play the stream from the specified 55 | application name and stream key combination 56 | """ 57 | @callback play_finished(Rtmp.ServerSession.Events.PlayStreamFinished.t, adopter_state) 58 | :: {:ok, adopter_state} 59 | 60 | @doc """ 61 | Called when a client publishing a stream has changed the metadata information 62 | for that stream. 63 | """ 64 | @callback metadata_received(Rtmp.ServerSession.Events.StreamMetaDataChanged.t, adopter_state) 65 | :: {:ok, adopter_state} 66 | 67 | @doc """ 68 | Called when audio or video data has been received on a published stream 69 | """ 70 | @callback audio_video_data_received(Rtmp.ServerSession.Events.AudioVideoDataReceived.t, adopter_state) 71 | :: {:ok, adopter_state} 72 | 73 | @doc """ 74 | Called when the number of bytes sent and received to the client changes 75 | """ 76 | @callback byte_io_totals_updated(Rtmp.ServerSession.Events.NewByteIOTotals.t, adopter_state) 77 | :: {:ok, adopter_state} 78 | 79 | @doc """ 80 | Called when the client sends an acknowledgement of bytes received 81 | """ 82 | @callback acknowledgement_received(Rtmp.ServerSession.Events.AcknowledgementReceived.t, adopter_state) 83 | :: {:ok, adopter_state} 84 | 85 | @doc """ 86 | Called when the server has successfully sent a ping request. This is needed to be handled 87 | if the server implementation wants track how long it's been since a ping request has gone 88 | unresponded to, or if the server wants to get an idea of latency 89 | """ 90 | @callback ping_request_sent(Rtmp.ServerSession.Events.PingRequestSent.t, adopter_state) 91 | :: {:ok, adopter_state} 92 | 93 | @doc """ 94 | Called when the server has received a response to a ping request. Note that unsolicited 95 | ping responses may come through, and it's up to the behavior implementor to decide how to 96 | react to it. 97 | """ 98 | @callback ping_response_received(Rtmp.ServerSession.Events.PingResponseReceived.t, adopter_state) 99 | :: {:ok, adopter_state} 100 | 101 | @doc "Called when an code change is ocurring" 102 | @callback code_change(any, adopter_state) :: {:ok, adopter_state} | {:error, String.t} 103 | 104 | @doc """ 105 | Called when any BEAM message is received that is not handleable by the generic RTMP server, 106 | and is thus being passed along to the module adopting this behaviour. 107 | """ 108 | @callback handle_message(any, adopter_state) :: {:ok, adopter_state} 109 | 110 | @doc """ 111 | Called when the TCP socket is closed. Allows for any last minute cleanup before 112 | the process is killed 113 | """ 114 | @callback handle_disconnection(adopter_state) :: {:ok, adopter_state} 115 | 116 | @spec start_link(module(), %GenRtmpServer.RtmpOptions{}, adopter_arguments) :: Supervisor.on_start 117 | @doc """ 118 | Starts the generic RTMP server using the provided RTMP options 119 | """ 120 | def start_link(module, options = %GenRtmpServer.RtmpOptions{}, additional_args \\ []) do 121 | {:ok, _} = Application.ensure_all_started(:ranch) 122 | 123 | _ = Logger.info "Starting RTMP listener on port #{options.port}" 124 | 125 | :ranch.start_listener(module, 126 | 10, 127 | :ranch_tcp, 128 | [port: options.port], 129 | GenRtmpServer.Protocol, 130 | [module, options, additional_args]) 131 | end 132 | 133 | @spec send_message(pid, outbound_data, stream_id, forced_timestamp) :: :ok 134 | @doc """ 135 | Signals a specific RTMP server process to send an RTMP message to its client 136 | """ 137 | def send_message(pid, outbound_data, stream_id, forced_timestamp \\ nil) do 138 | send(pid, {:rtmp_send, outbound_data, stream_id, forced_timestamp}) 139 | end 140 | 141 | @spec send_ping_request(pid) :: :ok 142 | @doc """ 143 | Sends a ping request to the client 144 | """ 145 | def send_ping_request(pid) do 146 | send(pid, :send_ping_request) 147 | end 148 | 149 | end 150 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/lib/gen_rtmp_server/audio_video_data.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServer.AudioVideoData do 2 | @type t :: %__MODULE__{ 3 | data_type: :audio | :video, 4 | received_at_timestamp: pos_integer(), 5 | data: <<>> 6 | } 7 | 8 | defstruct data_type: nil, 9 | received_at_timestamp: nil, 10 | data: <<>> 11 | 12 | end -------------------------------------------------------------------------------- /apps/gen_rtmp_server/lib/gen_rtmp_server/meta_data.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServer.MetaData do 2 | @type t :: %__MODULE__{ 3 | details: Rtmp.StreamMetadata.t 4 | } 5 | 6 | defstruct details: nil 7 | 8 | end -------------------------------------------------------------------------------- /apps/gen_rtmp_server/lib/gen_rtmp_server/rtmp_options.ex: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServer.RtmpOptions do 2 | @moduledoc """ 3 | Represents options that are available for starting an RTMP server 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | port: pos_integer(), 8 | fms_version: String.t, 9 | chunk_size: pos_integer(), 10 | log_mode: :none | :raw_io 11 | } 12 | 13 | @type options_list :: [port: pos_integer, fms_version: String.t, chunk_size: pos_integer] 14 | 15 | defstruct port: 1935, 16 | fms_version: "FMS/3,0,1,1233", 17 | chunk_size: 4096, 18 | log_mode: :none 19 | 20 | @spec to_keyword_list(%GenRtmpServer.RtmpOptions{}) :: options_list 21 | def to_keyword_list(options = %GenRtmpServer.RtmpOptions{}) do 22 | [ 23 | port: options.port, 24 | fms_version: options.fms_version, 25 | chunk_size: options.chunk_size, 26 | log_mode: options.log_mode 27 | ] 28 | end 29 | end -------------------------------------------------------------------------------- /apps/gen_rtmp_server/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServer.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :gen_rtmp_server, 7 | version: "0.2.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | description: description(), 16 | package: package(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | def application do 22 | [applications: [:logger, :ranch]] 23 | end 24 | 25 | defp deps do 26 | get_umbrella_dependencies(Mix.env) ++ 27 | [ 28 | get_ranch_dependency(Mix.env), 29 | {:uuid, "~> 1.1"}, 30 | {:ex_doc, "~> 0.14", only: [:dev, :publish, :umbrella]} 31 | ] 32 | end 33 | 34 | defp get_umbrella_dependencies(:umbrella) do 35 | [ 36 | {:rtmp, in_umbrella: true}, 37 | ] 38 | end 39 | 40 | defp get_umbrella_dependencies(_) do 41 | [ 42 | {:rtmp, "~> 0.2.0", hex: :eml_rtmp}, 43 | ] 44 | end 45 | 46 | defp get_ranch_dependency(:publish), do: {:ranch, "~> 1.2.1"} 47 | defp get_ranch_dependency(_), do: {:ranch, "~> 1.2.1", manager: :rebar} 48 | 49 | defp package do 50 | [ 51 | name: :eml_gen_rtmp_server, 52 | maintainers: ["Matthew Shapiro"], 53 | licenses: ["MIT"], 54 | links: %{"GitHub" => "https://github.com/KallDrexx/elixir-media-libs/tree/master/apps/gen_rtmp_server"} 55 | ] 56 | end 57 | 58 | defp description do 59 | "Behaviour to make it easy to create custom RTMP servers" 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/test/gen_rtmp_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GenRtmpServerTest do 2 | use ExUnit.Case 3 | doctest GenRtmpServer 4 | end 5 | -------------------------------------------------------------------------------- /apps/gen_rtmp_server/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/rtmp/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/rtmp/README.md: -------------------------------------------------------------------------------- 1 | # Rtmp 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add `rtmp` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [{:rtmp, "~> 0.1.0"}] 14 | end 15 | ``` 16 | 17 | 2. Ensure `rtmp` is started before your application: 18 | 19 | ```elixir 20 | def application do 21 | [applications: [:rtmp]] 22 | end 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /apps/rtmp/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :rtmp, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:rtmp, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp do 2 | 3 | @type connection_id :: String.t 4 | @type system_time_ms :: integer 5 | @type app_name :: String.t 6 | @type stream_key :: String.t 7 | @type packet_type :: :audio | :video | :misc 8 | 9 | @type deserialized_message :: Rtmp.Protocol.Messages.VideoData.t | 10 | Rtmp.Protocol.Messages.AudioData.t | 11 | Rtmp.Protocol.Messages.Abort.t | 12 | Rtmp.Protocol.Messages.Acknowledgement.t | 13 | Rtmp.Protocol.Messages.Amf0Command.t | 14 | Rtmp.Protocol.Messages.Amf0Data.t | 15 | Rtmp.Protocol.Messages.SetChunkSize.t | 16 | Rtmp.Protocol.Messages.SetPeerBandwidth.t | 17 | Rtmp.Protocol.Messages.UserControl.t | 18 | Rtmp.Protocol.Messages.WindowAcknowledgementSize.t 19 | 20 | defmodule Behaviours do 21 | @moduledoc false 22 | 23 | defmodule ProtocolHandler do 24 | @moduledoc "Behaviour for modules that can serialize and deserialize RTMP messages" 25 | 26 | @type protocol_handler_pid :: pid 27 | 28 | @callback notify_input(protocol_handler_pid, binary) :: :ok 29 | @callback send_message(protocol_handler_pid, Rtmp.Protocol.DetailedMessage.t) :: :ok 30 | end 31 | 32 | defmodule SessionHandler do 33 | @moduledoc "Behaviour for modules that can act as session handlers" 34 | 35 | @type session_handler_pid :: pid 36 | @type stream_id :: non_neg_integer 37 | @type forced_timestamp :: non_neg_integer | nil 38 | @type io_count_direction :: :bytes_received | :bytes_sent 39 | 40 | @callback handle_rtmp_input(session_handler_pid, Rtmp.Protocol.DetailedMessage.t) :: :ok 41 | @callback notify_byte_count(session_handler_pid, io_count_direction, non_neg_integer) :: :ok 42 | end 43 | 44 | defmodule EventReceiver do 45 | @moduledoc "Behaviour for modules receiving session events" 46 | 47 | @type event_receiver_pid :: pid 48 | @type event :: any 49 | 50 | @callback send_event(event_receiver_pid, event) :: :ok 51 | end 52 | 53 | defmodule SocketHandler do 54 | @moduledoc "Behaviour for modules receiving raw RTMP output to send" 55 | 56 | @type socket_handler_pid :: pid 57 | 58 | @callback send_data(pid, binary, Rtmp.packet_type) :: :ok 59 | end 60 | end 61 | 62 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/client_session/configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.ClientSession.Configuration do 2 | @type t :: %__MODULE__{ 3 | flash_version: String.t, 4 | playback_buffer_length_ms: non_neg_integer, 5 | window_ack_size: pos_integer 6 | } 7 | 8 | defstruct flash_version: "WIN 23,0,0,207", 9 | playback_buffer_length_ms: 2_000, 10 | window_ack_size: 2_500_000 11 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/client_session/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.ClientSession.Events do 2 | @moduledoc false 3 | 4 | @type t :: Rtmp.ClientSession.Events.ConnectionResponseReceived.t | 5 | Rtmp.ClientSession.Events.PublishResponseReceived.t | 6 | Rtmp.ClientSession.Events.PlayResponseReceived.t | 7 | Rtmp.ClientSession.Events.StreamMetaDataReceived.t | 8 | Rtmp.ClientSession.Events.AudioVideoDataReceived.t | 9 | Rtmp.ClientSession.Events.NewByteIOTotals.t | 10 | Rtmp.ClientSession.Events.PlayResetReceived.t 11 | 12 | defmodule ConnectionResponseReceived do 13 | @moduledoc """ 14 | Indicates that the server has accepted or rejcted the connection request 15 | """ 16 | 17 | @type t :: %__MODULE__{ 18 | was_accepted: boolean, 19 | response_text: String.t 20 | } 21 | 22 | defstruct was_accepted: nil, 23 | response_text: nil 24 | end 25 | 26 | defmodule PlayResponseReceived do 27 | @moduledoc """ 28 | Indicates that the server has accepted or rejected our request for playback 29 | of a stream key 30 | """ 31 | 32 | @type t :: %__MODULE__{ 33 | was_accepted: boolean, 34 | response_text: String.t, 35 | stream_key: Rtmp.stream_key 36 | } 37 | 38 | defstruct was_accepted: nil, 39 | response_text: nil, 40 | stream_key: nil 41 | end 42 | 43 | defmodule PublishResponseReceived do 44 | @moduledoc """ 45 | Indicates that the server has accepted or rejected our request for publishing 46 | on a specific stream key 47 | """ 48 | 49 | @type t :: %__MODULE__{ 50 | stream_key: Rtmp.stream_key, 51 | was_accepted: boolean, 52 | response_text: String.t, 53 | } 54 | 55 | defstruct stream_key: nil, 56 | was_accepted: nil, 57 | response_text: nil 58 | end 59 | 60 | defmodule StreamMetaDataReceived do 61 | @moduledoc """ 62 | Indicates that the server is reporting a change in the incoming stream's metadata 63 | """ 64 | 65 | @type t :: %__MODULE__{ 66 | meta_data: Rtmp.StreamMetadata.t, 67 | stream_key: Rtmp.stream_key 68 | } 69 | 70 | defstruct meta_data: nil, 71 | stream_key: nil 72 | end 73 | 74 | defmodule AudioVideoDataReceived do 75 | @moduledoc """ 76 | Indicates that audio or video data has been received 77 | """ 78 | 79 | @type t :: %__MODULE__{ 80 | stream_key: Rtmp.stream_key, 81 | data_type: :audio | :video, 82 | data: binary, 83 | timestamp: non_neg_integer, 84 | received_at_timestamp: pos_integer, 85 | } 86 | 87 | defstruct stream_key: nil, 88 | data_type: nil, 89 | data: <<>>, 90 | timestamp: nil, 91 | received_at_timestamp: nil 92 | end 93 | 94 | defmodule NewByteIOTotals do 95 | @moduledoc """ 96 | Event indicating the total number of bytes sent or received from the server has 97 | changed in value 98 | """ 99 | 100 | @type t :: %__MODULE__{ 101 | bytes_sent: non_neg_integer, 102 | bytes_received: non_neg_integer 103 | } 104 | 105 | defstruct bytes_received: 0, 106 | bytes_sent: 0 107 | end 108 | 109 | defmodule PlayResetReceived do 110 | @type t :: %__MODULE__{ 111 | stream_key: Rtmp.stream_key, 112 | description: String.t 113 | } 114 | 115 | defstruct stream_key: nil, 116 | description: nil 117 | end 118 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake do 2 | @moduledoc """ 3 | Provides functionality to handle the RTMP handshake process. 4 | 5 | ## Examples 6 | 7 | The following is an example of handling a handshake as a server: 8 | 9 | # Since we are a server we don't know what handshake type 10 | # the client will send 11 | {handshake, %RtmpHandshake.ParseResult{}} = RtmpHandshake.new(:unknown) 12 | 13 | c0_and_c1 = get_packets_0_and_1_from_client() 14 | {handshake, %RtmpHandshake.ParseResult{ 15 | bytes_to_send: bytes, 16 | current_state: :waiting_for_data 17 | }} = RtmpHandshake.process_bytes(handshake, c0_and_c1) 18 | 19 | send_bytes_to_client(bytes) 20 | c2 = get_packet_c2_from_client() 21 | 22 | {handshake, %RtmpHandshake.ParseResult{ 23 | current_state: :success 24 | }} = RtmpHandshake.process_bytes(handshake, c2) 25 | 26 | """ 27 | 28 | require Logger 29 | 30 | alias Rtmp.Handshake.OldHandshakeFormat, as: OldHandshakeFormat 31 | alias Rtmp.Handshake.ParseResult, as: ParseResult 32 | alias Rtmp.Handshake.HandshakeResult, as: HandshakeResult 33 | alias Rtmp.Handshake.DigestHandshakeFormat, as: DigestHandshakeFormat 34 | 35 | @type handshake_state :: %__MODULE__.State{} 36 | @type handshake_type :: :unknown | :old | :digest 37 | @type is_valid_format_result :: :yes | :no | :unknown 38 | @type start_time :: non_neg_integer 39 | @type remaining_binary :: <<>> 40 | @type binary_response :: <<>> 41 | @type behaviour_state :: any 42 | @type process_result :: {:success, start_time, binary_response, remaining_binary} 43 | | {:incomplete, binary_response} 44 | | :failure 45 | 46 | @callback is_valid_format(<<>>) :: is_valid_format_result 47 | @callback process_bytes(behaviour_state, <<>>) :: {behaviour_state, process_result} 48 | @callback create_p0_and_p1_to_send(behaviour_state) :: {behaviour_state, binary} 49 | 50 | defmodule State do 51 | @moduledoc false 52 | 53 | defstruct status: :pending, 54 | handshake_state: nil, 55 | handshake_type: :unknown, 56 | remaining_binary: <<>>, 57 | peer_start_timestamp: nil 58 | end 59 | 60 | @doc """ 61 | Creates a new finite state machine to handle the handshake process, 62 | and preliminary parse results. 63 | 64 | If a handshake type is specified we assume we are acting as a client 65 | (since a server won't know what type of handshake to use until it 66 | receives packets c0 and c1). 67 | """ 68 | @spec new(handshake_type) :: {handshake_state, ParseResult.t} 69 | def new(:old) do 70 | {handshake_state, bytes_to_send} = 71 | OldHandshakeFormat.new() 72 | |> OldHandshakeFormat.create_p0_and_p1_to_send() 73 | 74 | state = %State{handshake_type: :old, handshake_state: handshake_state} 75 | result = %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send} 76 | {state, result} 77 | end 78 | 79 | def new(:digest) do 80 | {handshake_state, bytes_to_send} = 81 | DigestHandshakeFormat.new() 82 | |> DigestHandshakeFormat.create_p0_and_p1_to_send() 83 | 84 | state = %State{handshake_type: :digest, handshake_state: handshake_state} 85 | result = %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send} 86 | {state, result} 87 | end 88 | 89 | def new(:unknown) do 90 | state = %State{handshake_type: :unknown} 91 | {state, %ParseResult{current_state: :waiting_for_data}} 92 | end 93 | 94 | @doc "Reads the passed in binary to proceed with the handshaking process" 95 | @spec process_bytes(handshake_state, <<>>) :: {handshake_state, ParseResult.t} 96 | def process_bytes(state = %State{handshake_type: :unknown}, binary) when is_binary(binary) do 97 | state = %{state | remaining_binary: state.remaining_binary <> binary} 98 | is_old_format = OldHandshakeFormat.is_valid_format(state.remaining_binary) 99 | is_digest_format = DigestHandshakeFormat.is_valid_format(state.remaining_binary) 100 | 101 | case {is_old_format, is_digest_format} do 102 | {_, :yes} -> 103 | handshake_state = DigestHandshakeFormat.new() 104 | 105 | binary = state.remaining_binary 106 | state = %{state | 107 | remaining_binary: <<>>, 108 | handshake_type: :digest, 109 | handshake_state: handshake_state 110 | } 111 | 112 | # Processing bytes should trigger p0 and p1 to be sent 113 | {state, result} = process_bytes(state, binary) 114 | result = %{result | bytes_to_send: result.bytes_to_send} 115 | {state, result} 116 | 117 | {:yes, _} -> 118 | {handshake_state, bytes_to_send} = 119 | OldHandshakeFormat.new() 120 | |> OldHandshakeFormat.create_p0_and_p1_to_send() 121 | 122 | binary = state.remaining_binary 123 | state = %{state | 124 | remaining_binary: <<>>, 125 | handshake_type: :old, 126 | handshake_state: handshake_state 127 | } 128 | 129 | {state, result} = process_bytes(state, binary) 130 | result = %{result | bytes_to_send: bytes_to_send <> result.bytes_to_send} 131 | {state, result} 132 | 133 | {:no, :no} -> 134 | # No known handhsake format 135 | {state, %ParseResult{current_state: :failure}} 136 | 137 | _ -> 138 | {state, %ParseResult{}} 139 | end 140 | end 141 | 142 | def process_bytes(state = %State{handshake_type: :old}, binary) when is_binary(binary) do 143 | case OldHandshakeFormat.process_bytes(state.handshake_state, binary) do 144 | {handshake_state, :failure} -> 145 | state = %{state | handshake_state: handshake_state} 146 | {state, %ParseResult{current_state: :failure}} 147 | 148 | {handshake_state, {:incomplete, bytes_to_send}} -> 149 | state = %{state | handshake_state: handshake_state} 150 | {state, %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}} 151 | 152 | {handshake_state, {:success, start_time, response, remaining_binary}} -> 153 | state = %{state | 154 | handshake_state: handshake_state, 155 | remaining_binary: remaining_binary, 156 | peer_start_timestamp: start_time, 157 | status: :complete 158 | } 159 | 160 | result = %ParseResult{current_state: :success, bytes_to_send: response} 161 | {state, result} 162 | end 163 | end 164 | 165 | def process_bytes(state = %State{handshake_type: :digest}, binary) when is_binary(binary) do 166 | case DigestHandshakeFormat.process_bytes(state.handshake_state, binary) do 167 | {handshake_state, :failure} -> 168 | state = %{state | handshake_state: handshake_state} 169 | {state, %ParseResult{current_state: :failure}} 170 | 171 | {handshake_state, {:incomplete, bytes_to_send}} -> 172 | state = %{state | handshake_state: handshake_state} 173 | {state, %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}} 174 | 175 | {handshake_state, {:success, start_time, response, remaining_binary}} -> 176 | state = %{state | 177 | handshake_state: handshake_state, 178 | remaining_binary: remaining_binary, 179 | peer_start_timestamp: start_time, 180 | status: :complete 181 | } 182 | 183 | result = %ParseResult{current_state: :success, bytes_to_send: response} 184 | {state, result} 185 | end 186 | end 187 | 188 | @doc """ 189 | After a handshake has been successfully completed this is called to 190 | retrieve the peer's starting timestamp and any left over binary that 191 | may need to be parsed later (not part of the handshake but instead 192 | the beginning of the rtmp protocol). 193 | """ 194 | @spec get_handshake_result(handshake_state) :: {handshake_state, HandshakeResult.t} 195 | def get_handshake_result(state = %State{status: :complete}) do 196 | unparsed_binary = state.remaining_binary 197 | 198 | { 199 | %{state | remaining_binary: <<>>}, 200 | %HandshakeResult{ 201 | peer_start_timestamp: state.peer_start_timestamp, 202 | remaining_binary: unparsed_binary 203 | } 204 | } 205 | end 206 | 207 | end 208 | -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake/digest_handshake_format.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.DigestHandshakeFormat do 2 | @moduledoc """ 3 | Functions to parse and validate RTMP handshakes based on flash client versions 4 | and SHA digests. This handshake is required for supporting H.264 video. 5 | 6 | Since no documentation of this handshake publicly exists from Adobe, this 7 | was created by referencing https://www.cs.cmu.edu/~dst/Adobe/Gallery/RTMPE.txt 8 | """ 9 | 10 | require Logger 11 | 12 | @random_crud <<0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8, 13 | 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57, 14 | 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab, 15 | 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae>> 16 | 17 | @genuine_fms_name "Genuine Adobe Flash Media Server 001" 18 | @genuine_player_name "Genuine Adobe Flash Player 001" 19 | @genuine_fms_with_crud @genuine_fms_name <> @random_crud 20 | @genuine_player_with_crud @genuine_player_name <> @random_crud 21 | 22 | @sha_256_digest_length 32 23 | 24 | @adobe_version <<128, 0, 7, 2>> # copied from jwplayer handshake 25 | 26 | @type state :: %__MODULE__.State{} 27 | 28 | defmodule State do 29 | @moduledoc false 30 | 31 | defstruct current_stage: :p0, 32 | unparsed_binary: <<>>, 33 | bytes_to_send: <<>>, 34 | received_start_time: 0, 35 | is_server: nil 36 | end 37 | 38 | @spec new() :: state 39 | @doc "Creates a new digest handshake format instance" 40 | def new() do 41 | %State{} 42 | end 43 | 44 | @spec is_valid_format(binary) :: :unknown | :yes | :no 45 | @doc "Validates if the passed in binary can be parsed using the digest handshake." 46 | def is_valid_format(binary) do 47 | cond do 48 | byte_size(binary) < 1537 -> :unknown 49 | <> = binary -> 50 | fms_version = get_message_format(c1, @genuine_fms_name) 51 | player_version = get_message_format(c1, @genuine_player_name) 52 | 53 | cond do 54 | type != 3 -> :no 55 | fms_version == :version1 || fms_version == :version2 -> :yes 56 | player_version == :version1 || player_version == :version2 -> :yes 57 | true -> :no 58 | end 59 | end 60 | end 61 | 62 | @spec process_bytes(state, binary) :: {state, Rtmp.Handshake.process_result} 63 | @doc "Attempts to proceed with the handshake process with the passed in bytes" 64 | def process_bytes(state = %State{}, binary) do 65 | state = %{state | unparsed_binary: state.unparsed_binary <> binary} 66 | do_process_bytes(state) 67 | end 68 | 69 | @spec create_p0_and_p1_to_send(state) :: {state, binary} 70 | @doc "Returns packets 0 and 1 to send to the peer" 71 | def create_p0_and_p1_to_send(state = %State{}) do 72 | random_binary = :crypto.strong_rand_bytes(1528) 73 | handshake = <<0::4 * 8>> <> @adobe_version <> random_binary 74 | 75 | {state, digest_offset, constant_key} = case state.is_server do 76 | nil -> 77 | # Since this is called prior to us knowing if we are a server or not 78 | # (i.e. we haven't received peer's packet 1 yet) we assume we are 79 | # the first to send a packet off and thus we are the client 80 | state = %{state | is_server: false} 81 | digest_offset = get_client_digest_offset(handshake) 82 | {state, digest_offset, @genuine_player_name} 83 | 84 | true -> 85 | digest_offset = get_server_digest_offset(handshake) 86 | {state, digest_offset, @genuine_fms_name} 87 | end 88 | 89 | {part1, _, part2} = get_message_parts(handshake, digest_offset) 90 | hmac = calc_hmac(part1, part2, constant_key) 91 | 92 | p0 = <<3::8>> 93 | p1 = part1 <> hmac <> part2 94 | {state, p0 <> p1} 95 | end 96 | 97 | defp do_process_bytes(state = %State{current_stage: :p0}) do 98 | if byte_size(state.unparsed_binary) < 1 do 99 | {state, {:incomplete, <<>>}} 100 | else 101 | <> = state.unparsed_binary 102 | case type do 103 | 3 -> 104 | state = %{state | unparsed_binary: rest, current_stage: :p1} 105 | do_process_bytes(state) 106 | 107 | _ -> 108 | {state, :failure} 109 | end 110 | end 111 | end 112 | 113 | defp do_process_bytes(state = %State{current_stage: :p1, is_server: nil}) do 114 | # Since is_server is nil, that means we got packet 1 from the peer before we sent 115 | # our packet 1. This means we are a server reacting to a client 116 | {state, p0_and_p1} = create_p0_and_p1_to_send(%{state | is_server: true}) 117 | state = %{state | bytes_to_send: state.bytes_to_send <> p0_and_p1 } 118 | 119 | do_process_bytes(state) 120 | end 121 | 122 | defp do_process_bytes(state = %State{current_stage: :p1}) do 123 | if byte_size(state.unparsed_binary) < 1536 do 124 | send_incomplete_response(state) 125 | else 126 | <> = state.unparsed_binary 127 | const_to_use = case state.is_server do 128 | true -> @genuine_player_name 129 | false -> @genuine_fms_name 130 | end 131 | 132 | {challenge_key_offset, key_offset} = case get_message_format(handshake, const_to_use) do 133 | :version1 -> {get_client_digest_offset(handshake), get_client_dh_offset(handshake)} 134 | :version2 -> {get_server_digest_offset(handshake), get_server_dh_offset(handshake)} 135 | end 136 | 137 | <<_::bytes-size(challenge_key_offset), challenge_key::bytes-size(32), _::binary>> = handshake 138 | 139 | key_offset_without_time = key_offset - 4 140 | << 141 | time::4 * 8, 142 | _::bytes-size(key_offset_without_time), 143 | _key::bytes-size(128), 144 | _::binary 145 | >> = handshake 146 | 147 | state = %{state | 148 | received_start_time: time, 149 | current_stage: :p2, 150 | bytes_to_send: state.bytes_to_send <> generate_p2(state.is_server, challenge_key), 151 | unparsed_binary: rest 152 | } 153 | 154 | do_process_bytes(state) 155 | end 156 | end 157 | 158 | defp do_process_bytes(state = %State{current_stage: :p2}) do 159 | if byte_size(state.unparsed_binary) < 1536 do 160 | send_incomplete_response(state) 161 | else 162 | # TODO: Add confirmation of the p1 public key we sent. For now 163 | # we are just assuming that if the peer didn't disconnect us we 164 | # are good 165 | 166 | <<_::1536 * 8, rest::binary>> = state.unparsed_binary 167 | state = %{state | unparsed_binary: rest} 168 | {state, {:success, state.received_start_time, state.bytes_to_send, state.unparsed_binary}} 169 | end 170 | end 171 | 172 | defp generate_p2(is_server, challenge_key) do 173 | random_binary = :crypto.strong_rand_bytes(1536 - @sha_256_digest_length) 174 | string = case is_server do 175 | true -> @genuine_fms_with_crud 176 | false -> @genuine_player_with_crud 177 | end 178 | 179 | digest = :crypto.hmac(:sha256, string, challenge_key) 180 | signature = :crypto.hmac(:sha256, digest, random_binary) 181 | 182 | random_binary <> signature 183 | end 184 | 185 | defp get_server_dh_offset(<<_::bytes-size(766), byte1, byte2, byte3, byte4, _::binary>>) do 186 | # Calculates the offset of the server's Diffie-Hellman key 187 | offset = byte1 + byte2 + byte3 + byte4 188 | rem(offset, 632) + 8 189 | end 190 | 191 | defp get_server_digest_offset(<<_::bytes-size(772), byte1, byte2, byte3, byte4, _::binary>>) do 192 | # Calculates the offset of the server's digest 193 | offset = byte1 + byte2 + byte3 + byte4 194 | rem(offset, 728) + 776 195 | end 196 | 197 | defp get_client_dh_offset(<<_::bytes-size(1532), byte1, byte2, byte3, byte4, _::binary>>) do 198 | # Calculates the offset of the client's Diffie-Hellmann key 199 | offset = byte1 + byte2 + byte3 + byte4 200 | rem(offset, 632) + 772 201 | end 202 | 203 | defp get_client_digest_offset(<<_::bytes-size(8), byte1, byte2, byte3, byte4, _::binary>>) do 204 | # Calculates the offset of the client's digest 205 | offset = byte1 + byte2 + byte3 + byte4 206 | rem(offset, 728) + 12 207 | end 208 | 209 | defp get_message_format(handshake, key) do 210 | version_1_offset = get_client_digest_offset(handshake) 211 | {v1_part1, v1_digest, v1_part2} = get_message_parts(handshake, version_1_offset) 212 | v1_hmac = calc_hmac(v1_part1, v1_part2, key) 213 | 214 | version_2_offset = get_server_digest_offset(handshake) 215 | {v2_part1, v2_digest, v2_part2} = get_message_parts(handshake, version_2_offset) 216 | v2_hmac = calc_hmac(v2_part1, v2_part2, key) 217 | 218 | cond do 219 | v1_hmac == v1_digest -> :version1 220 | v2_hmac == v2_digest -> :version2 221 | true -> :unknown 222 | end 223 | end 224 | 225 | defp get_message_parts(handshake, digest_offset) do 226 | after_digest = 1536 - (digest_offset + @sha_256_digest_length) 227 | 228 | << 229 | part1::bytes-size(digest_offset), 230 | digest::bytes-size(@sha_256_digest_length), 231 | part2::bytes-size(after_digest), 232 | _::binary 233 | >> = handshake 234 | 235 | {part1, digest, part2} 236 | end 237 | 238 | defp calc_hmac(part1, part2, key) do 239 | data = part1 <> part2 240 | :crypto.hmac(:sha256, key, data) 241 | end 242 | 243 | defp send_incomplete_response(state) do 244 | bytes_to_send = state.bytes_to_send 245 | state = %{state | bytes_to_send: <<>>} 246 | {state, {:incomplete, bytes_to_send}} 247 | end 248 | 249 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake/handshake_result.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.HandshakeResult do 2 | @moduledoc "Resulting information after processing a handshake operation" 3 | 4 | @type t :: %Rtmp.Handshake.HandshakeResult{ 5 | peer_start_timestamp: nil | non_neg_integer(), 6 | remaining_binary: binary 7 | } 8 | 9 | defstruct peer_start_timestamp: nil, 10 | remaining_binary: <<>> 11 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake/old_handshake_format.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.OldHandshakeFormat do 2 | @moduledoc """ 3 | Functions to parse and validate RTMP handshakes as specified in the 4 | official RTMP specification. 5 | 6 | This handshake format does *NOT* work for h.264 video. 7 | """ 8 | 9 | @behaviour Rtmp.Handshake 10 | 11 | require Logger 12 | 13 | @type state :: %__MODULE__.State{} 14 | 15 | defmodule State do 16 | @moduledoc false 17 | 18 | defstruct random_data: <<>>, 19 | current_stage: :p0, 20 | unparsed_binary: <<>>, 21 | bytes_to_send: <<>>, 22 | received_start_time: 0 23 | end 24 | 25 | @spec new() :: state 26 | @doc "Creates a new old handshake format instance" 27 | def new() do 28 | %State{} 29 | end 30 | 31 | @spec is_valid_format(binary) :: :unknown | :yes | :no 32 | @doc "Validates if the passed in binary can be parsed using the old style handshake." 33 | def is_valid_format(binary) do 34 | case byte_size(binary) >= 16 do 35 | false -> :unknown 36 | true -> 37 | case binary do 38 | <<3::1 * 8, _::4 * 8, 0::4 * 8, _::binary>> -> :yes 39 | _ -> :no 40 | end 41 | end 42 | end 43 | 44 | @spec process_bytes(state, binary) :: {state, Rtmp.Handshake.process_result} 45 | @doc "Attempts to proceed with the handshake process with the passed in bytes" 46 | def process_bytes(state = %State{}, binary) do 47 | state = %{state | unparsed_binary: state.unparsed_binary <> binary} 48 | do_process_bytes(state) 49 | end 50 | 51 | @spec create_p0_and_p1_to_send(state) :: {state, binary} 52 | @doc "Returns packets 0 and 1 to send to the peer" 53 | def create_p0_and_p1_to_send(state = %State{}) do 54 | state = %{state | random_data: :crypto.strong_rand_bytes(1528)} 55 | p0 = <<3::8>> 56 | p1 = <<0::4 * 8, 0::4 * 8>> <> state.random_data # local start time is alawys zero 57 | {state, p0 <> p1} 58 | end 59 | 60 | defp do_process_bytes(state = %State{current_stage: :p0}) do 61 | if byte_size(state.unparsed_binary) < 1 do 62 | send_incomplete_response(state) 63 | else 64 | case state.unparsed_binary do 65 | <<3::8, rest::binary>> -> 66 | state = %{state | 67 | unparsed_binary: rest, 68 | current_stage: :p1 69 | } 70 | 71 | do_process_bytes(state) 72 | 73 | _ -> 74 | {state, :failure} 75 | end 76 | end 77 | end 78 | 79 | defp do_process_bytes(state = %State{current_stage: :p1}) do 80 | if byte_size(state.unparsed_binary) < 1536 do 81 | send_incomplete_response(state) 82 | else 83 | case state.unparsed_binary do 84 | <> -> 85 | state = %{state | 86 | bytes_to_send: state.bytes_to_send <> <> <> random, # packet 2 87 | unparsed_binary: rest, 88 | received_start_time: time, 89 | current_stage: :p2 90 | } 91 | 92 | do_process_bytes(state) 93 | 94 | _ -> 95 | {state, :failure} 96 | end 97 | end 98 | end 99 | 100 | defp do_process_bytes(state = %State{current_stage: :p2}) do 101 | if byte_size(state.unparsed_binary) < 1536 do 102 | send_incomplete_response(state) 103 | else 104 | 105 | expected_random = state.random_data 106 | random_size = byte_size(expected_random) 107 | 108 | case state.unparsed_binary do 109 | <<0::4 * 8, _::4 * 8, ^expected_random::size(random_size)-binary, rest::binary>> -> 110 | bytes_to_send = state.bytes_to_send 111 | state = %{state | unparsed_binary: <<>>, current_stage: :complete, bytes_to_send: <<>>} 112 | {state, {:success, state.received_start_time, bytes_to_send, rest}} 113 | 114 | _ -> 115 | {state, :failure} 116 | end 117 | end 118 | end 119 | 120 | defp send_incomplete_response(state) do 121 | bytes_to_send = state.bytes_to_send 122 | state = %{state | bytes_to_send: <<>>} 123 | {state, {:incomplete, bytes_to_send}} 124 | end 125 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake/parse_result.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.ParseResult do 2 | @moduledoc """ 3 | Represents the current results from a parse operation 4 | """ 5 | 6 | @type t :: %Rtmp.Handshake.ParseResult{ 7 | current_state: :waiting_for_data | :failure | :success, 8 | bytes_to_send: <<>> 9 | } 10 | 11 | defstruct current_state: :waiting_for_data, 12 | bytes_to_send: <<>> 13 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/handshake/result.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.Result do 2 | @moduledoc false 3 | 4 | defstruct current_state: nil, 5 | bytes_to_send: <<>> 6 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/detailed_message.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.DetailedMessage do 2 | @moduledoc """ 3 | Represents the details of a deserialized RTMP message 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | timestamp: non_neg_integer, 8 | stream_id: non_neg_integer, 9 | content: Rtmp.deserialized_message, 10 | force_uncompressed: boolean, 11 | deserialization_system_time: pos_integer 12 | } 13 | 14 | defstruct timestamp: nil, 15 | stream_id: nil, 16 | content: nil, 17 | force_uncompressed: false, 18 | deserialization_system_time: nil 19 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Handler do 2 | @moduledoc """ 3 | This module controls the process that is responsible for serializing 4 | and deserializing RTMP chunk streams for a single peer in an RTMP 5 | connection. Input bytes come in, get deserialized into RTMP messages, 6 | and then get sent off to the specified session handling process. It can 7 | receive outbound RTMP messages that will then be serialized and sent off 8 | to the peer. 9 | 10 | Due to the way RTMP header compression works, it is expected that the 11 | protocol handler will receive every input byte of network communication 12 | after a successful handshake, and it will be the only system serializing 13 | and sending outbound RTMP messages to the peer. If these assumptions are 14 | broken then there is a large chance the client or server will crash due 15 | to not being able to properly parse an RTMP chunk correctly. 16 | """ 17 | 18 | require Logger 19 | use GenServer 20 | 21 | alias Rtmp.Protocol.ChunkIo, as: ChunkIo 22 | alias Rtmp.Protocol.RawMessage, as: RawMessage 23 | alias Rtmp.Protocol.DetailedMessage, as: DetailedMessage 24 | alias Rtmp.Protocol.Messages.SetChunkSize, as: SetChunkSize 25 | alias Rtmp.Protocol.Messages.VideoData, as: VideoData 26 | alias Rtmp.Protocol.Messages.AudioData, as: AudioData 27 | 28 | @type protocol_handler :: pid 29 | @type socket_transport_module :: module 30 | @type session_process :: pid 31 | @type session_handler_module :: module 32 | 33 | @behaviour Rtmp.Behaviours.ProtocolHandler 34 | 35 | defmodule State do 36 | @moduledoc false 37 | 38 | defstruct connection_id: nil, 39 | socket: nil, 40 | socket_module: nil, 41 | chunk_io_state: ChunkIo.new(), # nil, 42 | session_process: nil, 43 | session_module: nil, 44 | bytes_received: 0, 45 | bytes_sent: 0, 46 | last_bytes_sent_notification_at: 0, 47 | last_bytes_received_notification_at: 0, 48 | io_notification_timer: nil 49 | end 50 | 51 | @spec start_link(Rtmp.connection_id, Rtmp.Behaviours.SocketHandler.socket_handler_pid, socket_transport_module) :: {:ok, protocol_handler} 52 | @doc "Starts a new protocol handler process" 53 | def start_link(connection_id, socket, socket_module) do 54 | GenServer.start_link(__MODULE__, [connection_id, socket, socket_module]) 55 | end 56 | 57 | @spec set_session(protocol_handler, session_process, session_handler_module) :: :ok | :session_already_set 58 | @doc """ 59 | Specifies the session handler process and function to use to send deserialized 60 | RTMP messages to for the session handler. 61 | 62 | It is expected that the module that is passed in implements the 63 | `Rtmp.Behaviours.SessionHandler` behaviour. 64 | """ 65 | def set_session(pid, session_process, session_module) do 66 | GenServer.call(pid, {:set_session, {session_process, session_module}}) 67 | end 68 | 69 | @spec notify_input(protocol_handler, binary) :: :ok 70 | @doc """ 71 | Notifies the protocol handler of incoming binary coming in from the socket 72 | """ 73 | def notify_input(pid, binary) when is_binary(binary) do 74 | GenServer.cast(pid, {:socket_input, binary}) 75 | end 76 | 77 | @spec send_message(protocol_handler, DetailedMessage.t) :: :ok 78 | @doc """ 79 | Notifies the protocol handler of an rtmp message that should be serialized 80 | and sent to the peer. 81 | """ 82 | def send_message(pid, message = %DetailedMessage{}) do 83 | GenServer.cast(pid, {:send_message, message}) 84 | end 85 | 86 | def init([connection_id, socket, socket_module]) do 87 | state = %State{ 88 | connection_id: connection_id, 89 | socket: socket, 90 | socket_module: socket_module, 91 | chunk_io_state: ChunkIo.new() 92 | } 93 | {:ok, state} 94 | end 95 | 96 | def handle_call({:set_session, {pid, session_module}}, _from, state) do 97 | state = %{state | 98 | session_process: pid, 99 | session_module: session_module 100 | } 101 | 102 | {:reply, :ok, state} 103 | end 104 | 105 | def handle_cast({:socket_input, binary}, state) do 106 | if state.session_process == nil || state.session_module == nil do 107 | raise ("Input received, but session process and notification functions are not set yet") 108 | end 109 | 110 | state = process_bytes(state, binary) 111 | state = %{state | bytes_received: state.bytes_received + byte_size(binary)} 112 | state = trigger_io_notification_timer(state) 113 | 114 | {:noreply, state} 115 | end 116 | 117 | def handle_cast({:send_message, message}, state) do 118 | raw_message = RawMessage.pack(message) 119 | csid = get_csid_for_message_type(raw_message) 120 | 121 | {chunk_io_state, data} = ChunkIo.serialize(state.chunk_io_state, raw_message, csid) 122 | state = %{state | chunk_io_state: chunk_io_state} 123 | 124 | state = case message.content do 125 | %SetChunkSize{size: size} -> 126 | chunk_io_state = ChunkIo.set_sending_max_chunk_size(state.chunk_io_state, size) 127 | %{state | chunk_io_state: chunk_io_state} 128 | 129 | _ -> state 130 | end 131 | 132 | packet_type = case message.content do 133 | %VideoData{} -> :video 134 | %AudioData{} -> :audio 135 | _ -> :misc 136 | end 137 | 138 | :ok = state.socket_module.send_data(state.socket, data, packet_type) 139 | state = %{state | bytes_sent: state.bytes_sent + byte_size(data)} 140 | state = trigger_io_notification_timer(state) 141 | 142 | {:noreply, state} 143 | end 144 | 145 | def handle_info(:send_io_notifications, state) do 146 | if state.bytes_sent > state.last_bytes_sent_notification_at do 147 | :ok = state.session_module.notify_byte_count(state.session_process, :bytes_sent, state.bytes_sent) 148 | end 149 | 150 | if state.bytes_received > state.last_bytes_received_notification_at do 151 | :ok = state.session_module.notify_byte_count(state.session_process, :bytes_received, state.bytes_received) 152 | end 153 | 154 | state = %{state | 155 | io_notification_timer: nil, 156 | last_bytes_received_notification_at: state.bytes_received, 157 | last_bytes_sent_notification_at: state.bytes_sent 158 | } 159 | 160 | {:noreply, state} 161 | end 162 | 163 | defp process_bytes(state, binary) do 164 | {chunk_io_state, chunk_result} = ChunkIo.deserialize(state.chunk_io_state, binary) 165 | state = %{state | chunk_io_state: chunk_io_state} 166 | 167 | case chunk_result do 168 | :incomplete -> state 169 | :split_message -> process_bytes(state, <<>>) 170 | raw_message = %RawMessage{} -> act_on_message(state, raw_message) 171 | end 172 | end 173 | 174 | defp act_on_message(state, raw_message) do 175 | case RawMessage.unpack(raw_message) do 176 | {:error, :unknown_message_type} -> 177 | _ = Logger.error "#{state.connection_id}: Received message of type #{raw_message.message_type_id} but we have no known way to unpack it!" 178 | state 179 | 180 | {:ok, message = %DetailedMessage{content: %SetChunkSize{size: size}}} -> 181 | chunk_io_state = ChunkIo.set_receiving_max_chunk_size(state.chunk_io_state, size) 182 | state = %{state | chunk_io_state: chunk_io_state} 183 | 184 | :ok = state.session_module.handle_rtmp_input(state.session_process, message) 185 | process_bytes(state, <<>>) 186 | 187 | {:ok, message = %DetailedMessage{}} -> 188 | :ok = state.session_module.handle_rtmp_input(state.session_process, message) 189 | process_bytes(state, <<>>) 190 | end 191 | end 192 | 193 | defp trigger_io_notification_timer(state) do 194 | case state.io_notification_timer do 195 | nil -> 196 | :erlang.send_after(500, self(), :send_io_notifications) 197 | %{state | io_notification_timer: :active} 198 | 199 | _ -> state 200 | end 201 | end 202 | 203 | # Csid seems to mostly be for better utilizing compression by spreading 204 | # different message types among different chunk stream ids. It also allows 205 | # video and audio data to track different timestamps then other messages. 206 | # These numbers are just based on observations of current client-server activity 207 | defp get_csid_for_message_type(%RawMessage{message_type_id: 1}), do: 2 208 | defp get_csid_for_message_type(%RawMessage{message_type_id: 2}), do: 2 209 | defp get_csid_for_message_type(%RawMessage{message_type_id: 3}), do: 2 210 | defp get_csid_for_message_type(%RawMessage{message_type_id: 4}), do: 2 211 | defp get_csid_for_message_type(%RawMessage{message_type_id: 5}), do: 2 212 | defp get_csid_for_message_type(%RawMessage{message_type_id: 6}), do: 2 213 | defp get_csid_for_message_type(%RawMessage{message_type_id: 18}), do: 3 214 | defp get_csid_for_message_type(%RawMessage{message_type_id: 20}), do: 3 215 | defp get_csid_for_message_type(%RawMessage{message_type_id: 9}), do: 21 216 | defp get_csid_for_message_type(%RawMessage{message_type_id: 8}), do: 20 217 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/abort.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.Abort do 2 | @moduledoc """ 3 | 4 | Message used to notify the peer that if it is waiting 5 | for chunks to complete a message, then discard the partially 6 | received message 7 | 8 | """ 9 | 10 | @behaviour Rtmp.Protocol.RawMessage 11 | @type t :: %__MODULE__{} 12 | 13 | defstruct stream_id: nil 14 | 15 | def deserialize(data) do 16 | <> = data 17 | 18 | %__MODULE__{stream_id: stream_id} 19 | end 20 | 21 | def serialize(message = %__MODULE__{}) do 22 | {:ok, <>} 23 | end 24 | 25 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 26 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/acknowledgement.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.Acknowledgement do 2 | @moduledoc """ 3 | 4 | Sent when the client or the server receives bytes equal to the window 5 | size. 6 | 7 | Contains the number of bytes received so far (sequence number) 8 | 9 | """ 10 | 11 | @behaviour Rtmp.Protocol.RawMessage 12 | @type t :: %__MODULE__{} 13 | 14 | defstruct sequence_number: 0 15 | 16 | def deserialize(data) do 17 | <> = data 18 | 19 | %__MODULE__{sequence_number: sequence_number} 20 | end 21 | 22 | def serialize(message = %__MODULE__{}) do 23 | {:ok, <>} 24 | end 25 | 26 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 27 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/amf0_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.Amf0Command do 2 | @moduledoc """ 3 | Message used to denote an amf0 encoded command (or resposne to a command) 4 | """ 5 | 6 | @behaviour Rtmp.Protocol.RawMessage 7 | @type t :: %__MODULE__{} 8 | 9 | defstruct command_name: nil, 10 | transaction_id: nil, 11 | command_object: nil, 12 | additional_values: [] 13 | 14 | def deserialize(data) do 15 | {:ok, objects} = get_data(data) |> Amf0.deserialize() 16 | [command_name, transaction_id, command_object | rest] = objects 17 | 18 | %__MODULE__{ 19 | command_name: command_name, 20 | transaction_id: transaction_id, 21 | command_object: command_object, 22 | additional_values: rest 23 | } 24 | end 25 | 26 | def serialize(message = %__MODULE__{}) do 27 | objects = [message.command_name, message.transaction_id, message.command_object | message.additional_values] 28 | 29 | {:ok, Amf0.serialize(objects)} 30 | end 31 | 32 | def get_default_chunk_stream_id(%__MODULE__{}), do: 3 33 | 34 | # For some reason AMF3 commands are just AMF0 encoded commands with a zero in front of it 35 | # so remove the zero 36 | defp get_data(<<0::8, rest::binary>>), do: rest 37 | defp get_data(binary), do: binary 38 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/amf0_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.Amf0Data do 2 | @moduledoc """ 3 | Data structure containing metadata or user data, encoded in Amf0 4 | """ 5 | 6 | @behaviour Rtmp.Protocol.RawMessage 7 | @type t :: %__MODULE__{} 8 | 9 | defstruct parameters: [] 10 | 11 | def deserialize(data) do 12 | {:ok, objects} = Amf0.deserialize(data) 13 | 14 | %__MODULE__{parameters: objects} 15 | end 16 | 17 | def serialize(%__MODULE__{parameters: params}) do 18 | {:ok, Amf0.serialize(params)} 19 | end 20 | 21 | def get_default_chunk_stream_id(%__MODULE__{}), do: 3 22 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/audio_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.AudioData do 2 | @moduledoc """ 3 | Data structure containing audio data 4 | """ 5 | 6 | @behaviour Rtmp.Protocol.RawMessage 7 | @type t :: %__MODULE__{} 8 | 9 | defstruct data: <<>> 10 | 11 | def deserialize(data) do 12 | %__MODULE__{data: data} 13 | end 14 | 15 | def serialize(%__MODULE__{data: data}) do 16 | {:ok, data} 17 | end 18 | 19 | def get_default_chunk_stream_id(%__MODULE__{}), do: 5 20 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/set_chunk_size.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.SetChunkSize do 2 | @moduledoc """ 3 | Represents a message that the sender is changing their 4 | maximum chunk size 5 | """ 6 | 7 | @behaviour Rtmp.Protocol.RawMessage 8 | @type t :: %__MODULE__{ 9 | size: non_neg_integer() 10 | } 11 | 12 | defstruct size: 128 13 | 14 | def deserialize(data) do 15 | <<0::1, size_value::31>> = data 16 | 17 | %__MODULE__{size: size_value} 18 | end 19 | 20 | def serialize(message = %__MODULE__{}) do 21 | {:ok, <<0::1, message.size::31>>} 22 | end 23 | 24 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 25 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/set_peer_bandwidth.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.SetPeerBandwidth do 2 | @moduledoc """ 3 | 4 | Sender is requesting the receiver limit its output 5 | bandwidth by limiting the amount of sent but 6 | unacknowledged data to the specified window size 7 | 8 | """ 9 | 10 | defstruct window_size: 0, limit_type: nil 11 | 12 | @behaviour Rtmp.Protocol.RawMessage 13 | @type t :: %__MODULE__{} 14 | 15 | def deserialize(data) do 16 | <> = data 17 | 18 | %__MODULE__{window_size: size, limit_type: get_friendly_type(type)} 19 | end 20 | 21 | def serialize(message = %__MODULE__{}) do 22 | type = case message.limit_type do 23 | :hard -> 0 24 | :soft -> 1 25 | :dynamic -> 2 26 | end 27 | 28 | {:ok, <>} 29 | end 30 | 31 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 32 | 33 | defp get_friendly_type(0), do: :hard 34 | defp get_friendly_type(1), do: :soft 35 | defp get_friendly_type(2), do: :dynamic 36 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/user_control.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.UserControl do 2 | @moduledoc """ 3 | Message to notify the peer about user control events 4 | """ 5 | 6 | @behaviour Rtmp.Protocol.RawMessage 7 | @type t :: %__MODULE__{} 8 | 9 | defstruct type: nil, 10 | stream_id: nil, 11 | buffer_length: nil, 12 | timestamp: nil 13 | 14 | def deserialize(data) do 15 | <> = data 16 | 17 | parse(event_type, rest) 18 | end 19 | 20 | def serialize(message = %__MODULE__{}) do 21 | {:ok, serialize_data(message)} 22 | end 23 | 24 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 25 | 26 | defp parse(0, data) do 27 | <> = data 28 | %__MODULE__{type: :stream_begin, stream_id: stream_id} 29 | end 30 | 31 | defp parse(1, data) do 32 | <> = data 33 | %__MODULE__{type: :stream_eof, stream_id: stream_id} 34 | end 35 | 36 | defp parse(2, data) do 37 | <> = data 38 | %__MODULE__{type: :stream_dry, stream_id: stream_id} 39 | end 40 | 41 | defp parse(3, data) do 42 | <> = data 43 | %__MODULE__{type: :set_buffer_length, stream_id: stream_id, buffer_length: buffer_length} 44 | end 45 | 46 | defp parse(4, data) do 47 | <> = data 48 | %__MODULE__{type: :stream_is_recorded, stream_id: stream_id} 49 | end 50 | 51 | defp parse(6, data) do 52 | <> = data 53 | %__MODULE__{type: :ping_request, timestamp: timestamp} 54 | end 55 | 56 | defp parse(7, data) do 57 | <> = data 58 | %__MODULE__{type: :ping_response, timestamp: timestamp} 59 | end 60 | 61 | defp serialize_data(%__MODULE__{type: :stream_begin, stream_id: stream_id}) do 62 | <<0::16, stream_id::32>> 63 | end 64 | 65 | defp serialize_data(%__MODULE__{type: :stream_eof, stream_id: stream_id}) do 66 | <<1::16, stream_id::32>> 67 | end 68 | 69 | defp serialize_data(%__MODULE__{type: :stream_dry, stream_id: stream_id}) do 70 | <<2::16, stream_id::32>> 71 | end 72 | 73 | defp serialize_data(%__MODULE__{type: :set_buffer_length, stream_id: stream_id, buffer_length: buffer_length}) do 74 | <<3::16, stream_id::32, buffer_length::32>> 75 | end 76 | 77 | defp serialize_data(%__MODULE__{type: :stream_is_recorded, stream_id: stream_id}) do 78 | <<4::16, stream_id::32>> 79 | end 80 | 81 | defp serialize_data(%__MODULE__{type: :ping_request, timestamp: timestamp}) do 82 | <<6::16, timestamp::32>> 83 | end 84 | 85 | defp serialize_data(%__MODULE__{type: :ping_response, timestamp: timestamp}) do 86 | <<7::16, timestamp::32>> 87 | end 88 | 89 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/video_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.VideoData do 2 | @moduledoc """ 3 | Data structure containing video data 4 | """ 5 | 6 | @behaviour Rtmp.Protocol.RawMessage 7 | @type t :: %__MODULE__{} 8 | 9 | defstruct data: <<>> 10 | 11 | def deserialize(data) do 12 | %__MODULE__{data: data} 13 | end 14 | 15 | def serialize(%__MODULE__{data: data}) do 16 | {:ok, data} 17 | end 18 | 19 | def get_default_chunk_stream_id(%__MODULE__{}), do: 4 20 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/messages/window_acknowledgement_size.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.Messages.WindowAcknowledgementSize do 2 | @moduledoc """ 3 | Sent to inform the peer of a change in how much 4 | data the receiver should receive before the sender 5 | expects and acknowledgement message sent back 6 | """ 7 | 8 | @behaviour Rtmp.Protocol.RawMessage 9 | @type t :: %__MODULE__{} 10 | 11 | defstruct size: 0 12 | 13 | def deserialize(data) do 14 | <> = data 15 | 16 | %__MODULE__{size: size} 17 | end 18 | 19 | def serialize(message = %__MODULE__{}) do 20 | {:ok, <>} 21 | end 22 | 23 | def get_default_chunk_stream_id(%__MODULE__{}), do: 2 24 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/raw_message.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.RawMessage do 2 | @moduledoc """ 3 | Module that represents a raw RTMP message, and functions to unpack 4 | and repack them for handling and serialization 5 | """ 6 | 7 | alias Rtmp.Protocol.DetailedMessage, as: DetailedMessage 8 | 9 | @type t :: %__MODULE__{ 10 | timestamp: non_neg_integer(), 11 | message_type_id: non_neg_integer(), 12 | stream_id: non_neg_integer(), 13 | force_uncompressed: boolean(), 14 | deserialization_system_time: pos_integer() | nil, 15 | payload: binary() 16 | } 17 | 18 | defstruct timestamp: nil, 19 | message_type_id: nil, 20 | stream_id: nil, 21 | force_uncompressed: false, 22 | deserialization_system_time: nil, 23 | payload: <<>> 24 | 25 | @callback deserialize(binary) :: any 26 | @callback serialize(struct()) :: {:ok, binary()} 27 | @callback get_default_chunk_stream_id(struct()) :: pos_integer() 28 | 29 | @doc "Unpacks the specified RTMP message into it's proper structure" 30 | @spec unpack(__MODULE__.t) :: {:error, :unknown_message_type} | {:ok, DetailedMessage.t} 31 | def unpack(message = %__MODULE__{}) do 32 | case get_message_module(message.message_type_id) do 33 | nil -> {:error, :unknown_message_type} 34 | module -> 35 | {:ok, %DetailedMessage{ 36 | timestamp: message.timestamp, 37 | stream_id: message.stream_id, 38 | content: module.deserialize(message.payload), 39 | deserialization_system_time: message.deserialization_system_time 40 | }} 41 | end 42 | end 43 | 44 | @doc "Packs a detailed RTMP message into a serializable raw message" 45 | @spec pack(DetailedMessage.t) :: __MODULE__.t 46 | def pack(message = %DetailedMessage{}) do 47 | {:ok, payload} = message.content.__struct__.serialize(message.content) 48 | 49 | %__MODULE__{ 50 | timestamp: message.timestamp, 51 | stream_id: message.stream_id, 52 | message_type_id: get_message_type(message.content.__struct__), 53 | payload: payload, 54 | force_uncompressed: message.force_uncompressed 55 | } 56 | end 57 | 58 | defp get_message_module(1), do: Rtmp.Protocol.Messages.SetChunkSize 59 | defp get_message_module(2), do: Rtmp.Protocol.Messages.Abort 60 | defp get_message_module(3), do: Rtmp.Protocol.Messages.Acknowledgement 61 | defp get_message_module(4), do: Rtmp.Protocol.Messages.UserControl 62 | defp get_message_module(5), do: Rtmp.Protocol.Messages.WindowAcknowledgementSize 63 | defp get_message_module(6), do: Rtmp.Protocol.Messages.SetPeerBandwidth 64 | defp get_message_module(8), do: Rtmp.Protocol.Messages.AudioData 65 | defp get_message_module(9), do: Rtmp.Protocol.Messages.VideoData 66 | defp get_message_module(18), do: Rtmp.Protocol.Messages.Amf0Data 67 | defp get_message_module(20), do: Rtmp.Protocol.Messages.Amf0Command 68 | 69 | # I have no idea why but AMF3 messages are actually internally encoded 70 | # in AMF0, so just use amf0 for decoding 71 | defp get_message_module(17), do: Rtmp.Protocol.Messages.Amf0Command 72 | defp get_message_module(15), do: Rtmp.Protocol.Messages.Amf0Data 73 | 74 | defp get_message_module(_), do: nil 75 | 76 | # WARNING: We have to match on the module names themselves instead of 77 | # a normal struct pattern match, otherwise we have circular references 78 | # during compilation and it failes due to the callbacks 79 | defp get_message_type(Rtmp.Protocol.Messages.SetChunkSize), do: 1 80 | defp get_message_type(Rtmp.Protocol.Messages.Abort), do: 2 81 | defp get_message_type(Rtmp.Protocol.Messages.Acknowledgement), do: 3 82 | defp get_message_type(Rtmp.Protocol.Messages.UserControl), do: 4 83 | defp get_message_type(Rtmp.Protocol.Messages.WindowAcknowledgementSize), do: 5 84 | defp get_message_type(Rtmp.Protocol.Messages.SetPeerBandwidth), do: 6 85 | defp get_message_type(Rtmp.Protocol.Messages.AudioData), do: 8 86 | defp get_message_type(Rtmp.Protocol.Messages.VideoData), do: 9 87 | defp get_message_type(Rtmp.Protocol.Messages.Amf0Data), do: 18 88 | defp get_message_type(Rtmp.Protocol.Messages.Amf0Command), do: 20 89 | 90 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/protocol/rtmp_time.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.RtmpTime do 2 | @moduledoc """ 3 | Provides utilities to work with timestamps in an RTMP context. 4 | 5 | RTMP timestamps are 32 bits (unsigned) integers and thus roll over every ~50 days. 6 | All adjacent timestamps are within 2^31 - 1 milliseconds of 7 | each other (e.g. 10000 comes after 4000000000, and 3000000000 comes before 4000000000). 8 | 9 | """ 10 | 11 | @type rtmp_timestamp :: 0..2147483647 12 | 13 | @max_timestamp :math.pow(2, 32) 14 | @adjacent_threshold :math.pow(2, 31) - 1 15 | 16 | @doc """ 17 | Converts a timestamp into a valid RTMP timestamp 18 | (i.e. rolls it over if it's too high or too low) 19 | 20 | ## Examples 21 | 22 | iex> Rtmp.Protocol.RtmpTime.to_rtmp_timestamp(1000) 23 | 1000 24 | 25 | iex> Rtmp.Protocol.RtmpTime.to_rtmp_timestamp(-1000) 26 | 4294966296 27 | 28 | iex> Rtmp.Protocol.RtmpTime.to_rtmp_timestamp(4294968296) 29 | 1000 30 | """ 31 | def to_rtmp_timestamp(timestamp) when timestamp < 0, do: to_rtmp_timestamp(@max_timestamp + timestamp) 32 | def to_rtmp_timestamp(timestamp) when timestamp > @max_timestamp, do: to_rtmp_timestamp(timestamp - @max_timestamp) 33 | def to_rtmp_timestamp(timestamp), do: trunc(timestamp) 34 | 35 | @doc """ 36 | Applies the specified delta to a timestamp 37 | 38 | ## Examples 39 | 40 | iex> Rtmp.Protocol.RtmpTime.apply_delta(1000, 500) 41 | 1500 42 | 43 | iex> Rtmp.Protocol.RtmpTime.apply_delta(1000, -500) 44 | 500 45 | 46 | iex> Rtmp.Protocol.RtmpTime.apply_delta(1000, -2000) 47 | 4294966296 48 | 49 | iex> Rtmp.Protocol.RtmpTime.apply_delta(4294966296, 2000) 50 | 1000 51 | """ 52 | def apply_delta(timestamp, delta) do 53 | timestamp + delta 54 | |> to_rtmp_timestamp 55 | end 56 | 57 | 58 | @doc """ 59 | Gets the delta between an old RTMP timestamp and a new RTMP timestamp 60 | 61 | ## Examples 62 | 63 | iex> Rtmp.Protocol.RtmpTime.get_delta(4000000000, 4000001000) 64 | 1000 65 | 66 | iex> Rtmp.Protocol.RtmpTime.get_delta(4000000000, 10000) 67 | 294977296 68 | 69 | iex> Rtmp.Protocol.RtmpTime.get_delta(4000000000, 3000000000) 70 | -1000000000 71 | 72 | """ 73 | def get_delta(previous_timestamp, new_timestamp) do 74 | difference = new_timestamp - previous_timestamp 75 | is_adjacent = if :erlang.abs(difference) <= @adjacent_threshold, do: true, else: false 76 | 77 | do_get_delta(previous_timestamp, new_timestamp, is_adjacent) 78 | |> trunc 79 | end 80 | 81 | defp do_get_delta(timestamp1, timestamp2, true) do 82 | timestamp2 - timestamp1 83 | end 84 | 85 | defp do_get_delta(timestamp1, timestamp2, false) when timestamp1 > timestamp2 do 86 | (@max_timestamp - timestamp1) + timestamp2 87 | end 88 | 89 | defp do_get_delta(timestamp1, timestamp2, false) do 90 | (@max_timestamp - timestamp2) + timestamp1 91 | end 92 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/server_session/configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.ServerSession.Configuration do 2 | @moduledoc """ 3 | Represents configuration options that governs how an RTMP server session should operate 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | fms_version: String.t, 8 | chunk_size: pos_integer(), 9 | window_ack_size: pos_integer(), 10 | peer_bandwidth: pos_integer(), 11 | io_log_mode: :none | :raw_io 12 | } 13 | 14 | defstruct fms_version: "FMS/3,0,1,123", 15 | chunk_size: 4096, 16 | peer_bandwidth: 2500000, 17 | window_ack_size: 1073741824, 18 | io_log_mode: :none 19 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/server_session/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.ServerSession.Events do 2 | @moduledoc false 3 | 4 | @type t :: Rtmp.ServerSession.Events.PeerChunkSizeChanged.t | 5 | Rtmp.ServerSession.Events.ConnectionRequested.t | 6 | Rtmp.ServerSession.Events.ReleaseStreamRequested.t | 7 | Rtmp.ServerSession.Events.PublishStreamRequested.t | 8 | Rtmp.ServerSession.Events.StreamMetaDataChanged.t | 9 | Rtmp.ServerSession.Events.AudioVideoDataReceived.t | 10 | Rtmp.ServerSession.Events.UnhandleableAmf0Command.t | 11 | Rtmp.ServerSession.Events.PublishingFinished.t | 12 | Rtmp.ServerSession.Events.PlayStreamRequested.t | 13 | Rtmp.ServerSession.Events.PlayStreamFinished.t | 14 | Rtmp.ServerSession.Events.NewByteIOTotals.t | 15 | Rtmp.ServerSession.Events.AcknowledgementReceived.t | 16 | Rtmp.ServerSession.Events.PingResponseReceived.t | 17 | Rtmp.ServerSession.Events.PingRequestSent.t 18 | 19 | defmodule PeerChunkSizeChanged do 20 | @moduledoc """ 21 | Event indicating that the peer is changing the maximum size of the 22 | RTMP chunks they will be sending 23 | """ 24 | 25 | @type t :: %__MODULE__{ 26 | new_chunk_size: pos_integer 27 | } 28 | 29 | defstruct new_chunk_size: nil 30 | end 31 | 32 | defmodule ConnectionRequested do 33 | @moduledoc """ 34 | Event indicating that the peer is requesting a ConnectionRequested 35 | on the specified application name 36 | """ 37 | 38 | @type t :: %__MODULE__{ 39 | request_id: integer, 40 | app_name: Rtmp.app_name 41 | } 42 | 43 | defstruct request_id: nil, 44 | app_name: nil 45 | end 46 | 47 | defmodule ReleaseStreamRequested do 48 | @moduledoc """ 49 | Event indicating that the peer is requesting a stream key 50 | be released for use. 51 | """ 52 | 53 | @type t :: %__MODULE__{ 54 | request_id: integer, 55 | app_name: Rtmp.app_name, 56 | stream_key: Rtmp.stream_key 57 | } 58 | 59 | defstruct request_id: nil, 60 | app_name: nil, 61 | stream_key: nil 62 | end 63 | 64 | defmodule PublishStreamRequested do 65 | @moduledoc """ 66 | Event indicating that the peer is requesting the ability to 67 | publish on the specified stream key. 68 | """ 69 | 70 | @type t :: %__MODULE__{ 71 | request_id: integer, 72 | app_name: Rtmp.app_name, 73 | stream_key: Rtmp.stream_key, 74 | stream_id: non_neg_integer 75 | } 76 | 77 | defstruct request_id: nil, 78 | app_name: nil, 79 | stream_key: nil, 80 | stream_id: nil 81 | end 82 | 83 | defmodule PublishingFinished do 84 | @moduledoc """ 85 | Event indicating that the peer is finished publishing on the 86 | specified stream key. 87 | """ 88 | 89 | @type t :: %__MODULE__{ 90 | app_name: Rtmp.app_name, 91 | stream_key: Rtmp.stream_key 92 | } 93 | 94 | defstruct app_name: nil, 95 | stream_key: nil 96 | end 97 | 98 | defmodule StreamMetaDataChanged do 99 | @moduledoc """ 100 | Event indicating that the peer is changing metadata properties 101 | of the stream being published. 102 | """ 103 | 104 | @type t :: %__MODULE__{ 105 | app_name: Rtmp.app_name, 106 | stream_key: Rtmp.stream_key, 107 | meta_data: Rtmp.StreamMetadata.t 108 | } 109 | 110 | defstruct app_name: nil, 111 | stream_key: nil, 112 | meta_data: nil 113 | end 114 | 115 | defmodule AudioVideoDataReceived do 116 | @moduledoc """ 117 | Event indicating that audio or video data was received. 118 | """ 119 | 120 | @type t :: %__MODULE__{ 121 | app_name: Rtmp.app_name, 122 | stream_key: Rtmp.stream_key, 123 | data_type: :audio | :video, 124 | data: binary, 125 | timestamp: non_neg_integer, 126 | received_at_timestamp: pos_integer 127 | } 128 | 129 | defstruct app_name: nil, 130 | stream_key: nil, 131 | data_type: nil, 132 | data: <<>>, 133 | timestamp: nil, 134 | received_at_timestamp: nil 135 | end 136 | 137 | defmodule UnhandleableAmf0Command do 138 | @moduledoc """ 139 | Event indicating that an Amf0 command was received that was not able 140 | to be handled. 141 | """ 142 | 143 | @type t :: %__MODULE__{ 144 | command: %Rtmp.Protocol.Messages.Amf0Command{} 145 | } 146 | 147 | defstruct command: nil 148 | end 149 | 150 | defmodule PlayStreamRequested do 151 | @moduledoc """ 152 | Event indicating that the peer is requesting playback of the specified stream. 153 | """ 154 | 155 | @type video_type :: :live | :recorded | :any 156 | 157 | @type t :: %__MODULE__{ 158 | request_id: integer, 159 | app_name: Rtmp.app_name, 160 | stream_key: Rtmp.stream_key, 161 | video_type: video_type, 162 | start_at: non_neg_integer, 163 | duration: integer, 164 | reset: boolean, 165 | stream_id: non_neg_integer 166 | } 167 | 168 | defstruct request_id: nil, 169 | app_name: nil, 170 | stream_key: nil, 171 | video_type: nil, 172 | start_at: nil, 173 | duration: nil, 174 | reset: nil, 175 | stream_id: nil 176 | end 177 | 178 | defmodule PlayStreamFinished do 179 | @moduledoc """ 180 | Event indicating that they are finished with playback of the specified stream. 181 | """ 182 | 183 | @type t :: %__MODULE__{ 184 | app_name: Rtmp.app_name, 185 | stream_key: Rtmp.stream_key 186 | } 187 | 188 | defstruct app_name: nil, 189 | stream_key: nil 190 | end 191 | 192 | defmodule NewByteIOTotals do 193 | @moduledoc """ 194 | Event indicating the total number of bytes sent or received from the client has 195 | changed in value 196 | """ 197 | 198 | @type t :: %__MODULE__{ 199 | bytes_sent: non_neg_integer, 200 | bytes_received: non_neg_integer 201 | } 202 | 203 | defstruct bytes_received: 0, 204 | bytes_sent: 0 205 | end 206 | 207 | defmodule AcknowledgementReceived do 208 | @moduledoc """ 209 | Event indicating that the client has sent an acknowledgement that they have received 210 | the specified number of bytes 211 | """ 212 | 213 | @type t :: %__MODULE__{ 214 | bytes_received: non_neg_integer 215 | } 216 | 217 | defstruct bytes_received: 0 218 | end 219 | 220 | defmodule PingRequestSent do 221 | @moduledoc """ 222 | Event indicating that the server has sent a ping request to the client 223 | """ 224 | 225 | @type t :: %__MODULE__{ 226 | timestamp: non_neg_integer 227 | } 228 | 229 | defstruct timestamp: nil 230 | end 231 | 232 | defmodule PingResponseReceived do 233 | @moduledoc """ 234 | Event indicating that the client has responded to a ping request 235 | """ 236 | 237 | @type t :: %__MODULE__{ 238 | timestamp: non_neg_integer 239 | } 240 | 241 | defstruct timestamp: nil 242 | end 243 | 244 | end -------------------------------------------------------------------------------- /apps/rtmp/lib/rtmp/stream_metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.StreamMetadata do 2 | @type t :: %__MODULE__{ 3 | video_width: nil | pos_integer(), 4 | video_height: nil | pos_integer(), 5 | video_codec: nil | String.t, 6 | video_frame_rate: nil | float(), 7 | video_bitrate_kbps: nil | pos_integer(), 8 | audio_codec: nil | String.t, 9 | audio_bitrate_kbps: nil | pos_integer(), 10 | audio_sample_rate: nil | pos_integer(), 11 | audio_channels: nil | pos_integer(), 12 | audio_is_stereo: nil | boolean(), 13 | encoder: nil | String.t 14 | } 15 | 16 | defstruct video_width: nil, 17 | video_height: nil, 18 | video_codec: nil, 19 | video_frame_rate: nil, 20 | video_bitrate_kbps: nil, 21 | audio_codec: nil, 22 | audio_bitrate_kbps: nil, 23 | audio_sample_rate: nil, 24 | audio_channels: nil, 25 | audio_is_stereo: nil, 26 | encoder: nil 27 | 28 | end -------------------------------------------------------------------------------- /apps/rtmp/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rtmp, 7 | version: "0.2.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.3", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps(), 16 | package: package(), 17 | description: description() 18 | ] 19 | end 20 | 21 | def application do 22 | [applications: [:logger]] 23 | end 24 | 25 | defp deps do 26 | environment_specific_deps(Mix.env) ++ [ 27 | {:ex_doc, ">= 0.0.0", only: [:dev, :umbrella]} 28 | ] 29 | end 30 | 31 | defp environment_specific_deps(:umbrella), do: [{:amf0, in_umbrella: true}] 32 | defp environment_specific_deps(_), do: [{:amf0, "~> 1.0.1", hex: :eml_amf0}] 33 | 34 | defp package do 35 | [ 36 | name: :eml_rtmp, 37 | maintainers: ["Matthew Shapiro"], 38 | licenses: ["MIT"], 39 | links: %{"GitHub" => "https://github.com/KallDrexx/elixir-media-libs/tree/master/apps/rtmp"} 40 | ] 41 | end 42 | 43 | defp description do 44 | "Library containing functionality for handling RTMP connections, from handshaking, " <> 45 | "serialization, deserialization, and logical flow of RTMP data." 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /apps/rtmp/test/handshake/digest_handshake_format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.DigestHandshakeFormatTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Rtmp.Handshake.DigestHandshakeFormat, as: DigestHandshakeFormat 5 | 6 | @jwplayer_c0 <<0x03>> 7 | @jwplayer_c1 <<0x00, 0x12, 0x6c, 0xbb, 0x80, 0x00, 0x07, 0x02, 0x62, 0x3f, 0x16, 0x27, 0xc6, 0x1d, 0xac, 8 | 0x34, 0x38, 0x46, 0xf2, 0xbc, 0x67, 0xce, 0xed, 0xac, 0xe3, 0x00, 0x0d, 0x73, 0x54, 0x46, 0x03, 9 | 0x95, 0xba, 0xc3, 0x3b, 0xd7, 0xf5, 0xa4, 0x40, 0x5f, 0xa9, 0xd4, 0x7b, 0x0d, 0x91, 0xd9, 0x98, 10 | 0xbd, 0xaf, 0x21, 0xd0, 0x3d, 0xd4, 0xf0, 0x2f, 0x91, 0x47, 0xd3, 0xd4, 0x70, 0x5b, 0xd4, 0xd0, 11 | 0x67, 0x84, 0x64, 0x2b, 0x0d, 0x29, 0x66, 0xbd, 0x02, 0x09, 0x86, 0x8b, 0x64, 0x0d, 0x45, 0x01, 12 | 0xb0, 0xf8, 0xa7, 0xca, 0x0e, 0xa2, 0x47, 0x6d, 0x2a, 0xec, 0x94, 0xc0, 0xc8, 0x75, 0x1f, 0x44, 13 | 0x64, 0xb5, 0xa9, 0x18, 0x3c, 0x81, 0xcb, 0x86, 0xc0, 0x6d, 0xe6, 0x93, 0x9d, 0x86, 0x5c, 0x96, 14 | 0x43, 0xc7, 0xac, 0x53, 0xf7, 0xb8, 0xf8, 0xbb, 0x5d, 0x73, 0xa7, 0x5a, 0x3a, 0x78, 0xf2, 0xaa, 15 | 0x88, 0x21, 0x2d, 0x78, 0x0f, 0x86, 0xc0, 0xcb, 0x61, 0x0d, 0x03, 0xcf, 0x54, 0x81, 0xc7, 0xab, 16 | 0xd3, 0x76, 0xb6, 0x13, 0x38, 0x03, 0xcf, 0x53, 0x96, 0x41, 0xa3, 0xc9, 0xbc, 0x8b, 0x48, 0x2a, 17 | 0x58, 0xc9, 0xd3, 0xf3, 0x65, 0x96, 0x96, 0x0f, 0x1c, 0x8a, 0x88, 0xb3, 0x7c, 0xbb, 0x53, 0x40, 18 | 0x53, 0x47, 0xa2, 0xf8, 0xbe, 0x57, 0xe1, 0x8a, 0x3b, 0xc1, 0xf6, 0xdc, 0x97, 0x32, 0xfb, 0xeb, 19 | 0x4b, 0x06, 0x8e, 0x70, 0x68, 0x71, 0x84, 0x71, 0xdc, 0x6e, 0xae, 0x54, 0xa5, 0xa7, 0xb7, 0x18, 20 | 0xf8, 0xdf, 0x89, 0xab, 0x1f, 0x04, 0x64, 0xa3, 0xc1, 0x40, 0x82, 0xab, 0x8d, 0x7f, 0x41, 0xac, 21 | 0xdd, 0xc5, 0x2c, 0xe1, 0xe5, 0x45, 0x6f, 0x00, 0x72, 0xdf, 0x49, 0xe8, 0x7a, 0x09, 0x34, 0xa3, 22 | 0xce, 0xb9, 0x06, 0xd4, 0x09, 0x45, 0x48, 0x07, 0x9b, 0x82, 0x9a, 0xab, 0xff, 0xf8, 0x86, 0x97, 23 | 0xc3, 0x90, 0xd1, 0x1d, 0x24, 0xe9, 0x81, 0x3b, 0x22, 0x5f, 0xb1, 0x01, 0x47, 0xb7, 0xb0, 0xa4, 24 | 0xc7, 0x79, 0x4c, 0xf7, 0xae, 0x09, 0xdc, 0x34, 0xe9, 0x25, 0x2c, 0x7c, 0x46, 0x7b, 0x1b, 0x02, 25 | 0x7c, 0x07, 0x2a, 0xa2, 0x6c, 0xce, 0xcc, 0x01, 0xfe, 0xa2, 0x02, 0xbb, 0xc1, 0x5d, 0x41, 0x21, 26 | 0xea, 0xd7, 0x95, 0x9e, 0x26, 0xfe, 0x8d, 0xdb, 0xe8, 0x33, 0x9a, 0xf9, 0x0e, 0x1b, 0x00, 0xa7, 27 | 0x28, 0x84, 0x52, 0xd8, 0x30, 0xb5, 0x05, 0xba, 0x87, 0xa9, 0x23, 0xe3, 0x46, 0xa5, 0x78, 0x10, 28 | 0x7a, 0xe5, 0xa9, 0xcc, 0xf1, 0xa5, 0xae, 0x95, 0xe8, 0xd0, 0xe4, 0xc3, 0x43, 0xc4, 0x45, 0x9c, 29 | 0x4e, 0xcd, 0xa3, 0x8c, 0x52, 0xc8, 0x94, 0x6c, 0x86, 0xab, 0x77, 0xa4, 0xde, 0x39, 0x0f, 0x7b, 30 | 0x98, 0x0b, 0xd3, 0x94, 0xe4, 0x21, 0x40, 0xb5, 0x0d, 0xc1, 0x01, 0x94, 0x83, 0xa4, 0xc8, 0xf2, 31 | 0x27, 0xda, 0x7f, 0x3f, 0x8a, 0xce, 0xfa, 0x1d, 0x2c, 0xa2, 0x39, 0xa0, 0x8a, 0x73, 0x87, 0x87, 32 | 0x9f, 0x9f, 0xc8, 0xa2, 0xa4, 0x0a, 0x07, 0x88, 0x0d, 0x98, 0x8e, 0xd5, 0xcb, 0x1b, 0x2b, 0x00, 33 | 0x7a, 0xbb, 0xaf, 0xce, 0x8a, 0x54, 0x52, 0x35, 0x37, 0x64, 0xc3, 0x6c, 0xbc, 0x07, 0xe5, 0x70, 34 | 0x13, 0x1b, 0x24, 0xa6, 0x9c, 0x48, 0xc4, 0xa4, 0x3f, 0x38, 0xd6, 0x22, 0x98, 0x89, 0x9c, 0x38, 35 | 0x03, 0xdc, 0x1e, 0x44, 0xcf, 0xe9, 0x6c, 0x5e, 0x48, 0x9a, 0x33, 0xc4, 0x9f, 0xb9, 0xc0, 0xbe, 36 | 0x79, 0x6d, 0x4c, 0x9e, 0x82, 0xab, 0x61, 0x6a, 0xd3, 0x95, 0x1d, 0x56, 0xd2, 0x12, 0xbc, 0x3b, 37 | 0x15, 0x9c, 0x1e, 0x95, 0x0a, 0x36, 0x2c, 0x1e, 0xfd, 0xcb, 0x73, 0x46, 0x4e, 0x4c, 0xe5, 0x53, 38 | 0x63, 0xae, 0xf1, 0x96, 0xe4, 0x76, 0x75, 0x28, 0x36, 0x94, 0xc9, 0xb6, 0x35, 0xb7, 0x5a, 0x32, 39 | 0xfa, 0xd1, 0x7c, 0xe5, 0x80, 0x0b, 0x33, 0x0c, 0xaa, 0x35, 0xbf, 0x96, 0xc0, 0xe5, 0x02, 0x55, 40 | 0x80, 0x97, 0x68, 0x6d, 0xf5, 0x52, 0xb3, 0x4b, 0x77, 0x0c, 0x1b, 0x8a, 0x55, 0xcd, 0xa0, 0x88, 41 | 0x84, 0xce, 0x02, 0x6c, 0x99, 0x76, 0x91, 0x7a, 0x61, 0x79, 0x3a, 0xc1, 0x66, 0xcd, 0xe9, 0x36, 42 | 0x73, 0x2d, 0x41, 0xd2, 0x2b, 0x05, 0xc4, 0x88, 0x11, 0x74, 0x24, 0x83, 0x50, 0xed, 0x37, 0x5e, 43 | 0xc5, 0xc3, 0xfa, 0x84, 0x4d, 0x81, 0xf3, 0x2d, 0xf7, 0xf0, 0xfd, 0x08, 0xbc, 0x10, 0x9e, 0xe2, 44 | 0xef, 0xdb, 0x4f, 0xcb, 0x6e, 0x9e, 0x14, 0x28, 0x39, 0x3a, 0x9a, 0xfa, 0x49, 0xf8, 0x63, 0x63, 45 | 0x8e, 0xa7, 0xe1, 0xb6, 0xdf, 0x37, 0xbd, 0xd7, 0xa6, 0xfd, 0xcf, 0x40, 0x40, 0x3d, 0x00, 0xb8, 46 | 0x5b, 0x44, 0x40, 0x82, 0x3e, 0x49, 0x9d, 0xcb, 0xf5, 0xaa, 0x30, 0x08, 0x04, 0x95, 0x39, 0x87, 47 | 0xb9, 0x1f, 0xb3, 0xb7, 0xfc, 0xe4, 0x72, 0x1e, 0xbc, 0x82, 0x7b, 0x16, 0x7f, 0x2c, 0xea, 0x06, 48 | 0x9e, 0x5c, 0xb1, 0xb7, 0x34, 0x46, 0x62, 0x11, 0xf6, 0x1e, 0x4a, 0xcd, 0xeb, 0xa8, 0xed, 0x1a, 49 | 0xb6, 0x51, 0xc3, 0x68, 0xfb, 0x31, 0x2d, 0x9d, 0x84, 0x21, 0x9e, 0x96, 0xbf, 0xe5, 0x1b, 0x6b, 50 | 0x7b, 0x83, 0x47, 0xdd, 0x45, 0xff, 0xc2, 0x70, 0x5d, 0xc3, 0xa5, 0x1d, 0x6b, 0x79, 0x27, 0xd1, 51 | 0x6d, 0x45, 0x47, 0x7b, 0x25, 0xaf, 0xed, 0x58, 0x1d, 0x8f, 0x2d, 0xcd, 0xeb, 0x25, 0x5e, 0x62, 52 | 0x68, 0x5f, 0x33, 0xf3, 0x50, 0x81, 0x0f, 0x5f, 0x95, 0x85, 0xf9, 0x99, 0x05, 0x1d, 0xff, 0x6c, 53 | 0x9a, 0x9e, 0x3d, 0x3d, 0xd1, 0x1f, 0x53, 0x3a, 0x2e, 0x26, 0x2e, 0x6b, 0xda, 0xb5, 0x41, 0x6d, 54 | 0x36, 0x45, 0x57, 0x1f, 0x0f, 0xea, 0x24, 0x3e, 0xce, 0x54, 0x79, 0x25, 0x8a, 0x9c, 0x27, 0xe8, 55 | 0x72, 0x27, 0x74, 0x4e, 0x05, 0x71, 0x01, 0x9f, 0x68, 0xdf, 0x44, 0xc7, 0x25, 0xc8, 0xbc, 0x95, 56 | 0x7f, 0x33, 0xea, 0x08, 0xa9, 0xc4, 0x40, 0x15, 0x93, 0xac, 0x69, 0x04, 0x8e, 0xd9, 0xb1, 0x98, 57 | 0x18, 0xff, 0x16, 0x33, 0x61, 0x18, 0xb3, 0x08, 0xd0, 0x84, 0x8c, 0x49, 0xdc, 0x22, 0x2b, 0x9c, 58 | 0x09, 0xc5, 0x56, 0x97, 0xed, 0x80, 0xeb, 0x03, 0xba, 0x66, 0x33, 0xdc, 0xf9, 0x7a, 0xea, 0xff, 59 | 0xc6, 0x27, 0xef, 0xd6, 0x02, 0x4e, 0x1b, 0xa7, 0x2d, 0xfb, 0x58, 0xd7, 0xe8, 0x55, 0x48, 0x4b, 60 | 0x85, 0xb2, 0x0c, 0xea, 0xac, 0x66, 0x59, 0x12, 0x0e, 0xcc, 0x08, 0xb9, 0x1e, 0x08, 0xdb, 0x7b, 61 | 0x01, 0x60, 0x70, 0xb7, 0xd2, 0x49, 0x62, 0x5b, 0x4e, 0x45, 0x9e, 0xf4, 0xf4, 0x9c, 0x73, 0xbd, 62 | 0x20, 0xaf, 0xaf, 0xc2, 0xb9, 0xcb, 0x37, 0x10, 0x92, 0xed, 0x8a, 0x62, 0x11, 0x64, 0x66, 0xf4, 63 | 0xe2, 0x59, 0x7e, 0xaa, 0x24, 0x76, 0x64, 0x18, 0xab, 0x34, 0x6d, 0x18, 0xc8, 0xc9, 0x1f, 0xba, 64 | 0x62, 0x03, 0x01, 0xa9, 0xfb, 0xe3, 0xe5, 0x15, 0x06, 0x9d, 0xb0, 0x8f, 0x49, 0xa3, 0x4f, 0x91, 65 | 0x44, 0x3a, 0xd5, 0x25, 0xd0, 0x55, 0x52, 0x0f, 0x6b, 0x19, 0x30, 0xfb, 0x9b, 0x2a, 0x47, 0xef, 66 | 0xbb, 0xdd, 0x36, 0x36, 0xad, 0x66, 0x91, 0x6f, 0x88, 0xe9, 0xd2, 0xb4, 0x2d, 0xcd, 0x99, 0xd2, 67 | 0xb7, 0x0a, 0xec, 0xa1, 0x6c, 0xba, 0xdb, 0xf8, 0x6a, 0xd7, 0xed, 0x82, 0xd3, 0x72, 0x94, 0x4c, 68 | 0x57, 0x5f, 0x9a, 0xaa, 0xb4, 0x04, 0x92, 0x52, 0x36, 0xca, 0x11, 0xef, 0x81, 0x7a, 0x83, 0xa8, 69 | 0x87, 0x24, 0x6d, 0xe2, 0x10, 0x43, 0xd4, 0xe2, 0x9e, 0x25, 0x37, 0x83, 0xdc, 0x72, 0x7f, 0x63, 70 | 0x19, 0xf8, 0x2a, 0x84, 0x94, 0x6c, 0xf2, 0xf6, 0xaf, 0x4a, 0x53, 0x28, 0xd8, 0xb8, 0x5e, 0xd0, 71 | 0x1e, 0x45, 0x65, 0x43, 0xbd, 0x72, 0x4b, 0x55, 0x0a, 0x00, 0xac, 0x39, 0x42, 0xdc, 0xef, 0x9b, 72 | 0x25, 0x4e, 0x36, 0x61, 0x2f, 0x0d, 0xdb, 0x80, 0x0f, 0x8f, 0xe6, 0x1e, 0x0e, 0xd2, 0x7e, 0x12, 73 | 0x28, 0x56, 0xf5, 0x33, 0x8c, 0xa8, 0x6e, 0xfe, 0x63, 0x7f, 0xfb, 0x2e, 0xf7, 0xde, 0x0e, 0x7c, 74 | 0xd9, 0x4c, 0xa4, 0x8d, 0xb7, 0x69, 0xef, 0xac, 0x6e, 0x74, 0x0c, 0x85, 0x75, 0xdc, 0x57, 0x80, 75 | 0xa0, 0x2e, 0xca, 0xf4, 0x8a, 0x17, 0x0e, 0x21, 0x0e, 0x7c, 0x33, 0xa3, 0x8d, 0xfe, 0xb3, 0xdf, 76 | 0x5f, 0x7d, 0x8b, 0xe5, 0x84, 0x26, 0x1a, 0x3d, 0x1a, 0x76, 0x8a, 0x06, 0x0d, 0xb0, 0xb1, 0x95, 77 | 0xe9, 0x14, 0x61, 0x3a, 0xfb, 0xf6, 0xce, 0x8b, 0x5d, 0x6f, 0x5a, 0x91, 0xc3, 0x32, 0x65, 0xb3, 78 | 0x1c, 0xfa, 0xfb, 0xbe, 0xd7, 0x2f, 0xe9, 0xd0, 0xa8, 0x24, 0x0a, 0x66, 0xc7, 0x60, 0xdf, 0xdc, 79 | 0x83, 0x21, 0xb2, 0x28, 0x2b, 0x94, 0xee, 0x94, 0x6d, 0xa6, 0x21, 0x4e, 0x07, 0xd1, 0xe8, 0x6b, 80 | 0x1d, 0xe9, 0xd3, 0x00, 0xca, 0xca, 0x4c, 0xd2, 0x98, 0x7b, 0xd0, 0x37, 0xde, 0x78, 0xfd, 0x84, 81 | 0x0e, 0xf1, 0x54, 0x6d, 0x2c, 0x26, 0x82, 0x53, 0x37, 0x01, 0x01, 0x23, 0x67, 0x4a, 0x78, 0xa6, 82 | 0x12, 0x49, 0x15, 0xb9, 0x25, 0x87, 0x06, 0x8e, 0xe7, 0xaf, 0x24, 0x41, 0x5e, 0x9e, 0x8d, 0x27, 83 | 0x93, 0xa6, 0x80, 0xae, 0x72, 0xa0, 0x7c, 0x7b, 0x46, 0xd2, 0x1e, 0xcc, 0x4e, 0xb7, 0xb5, 0x17, 84 | 0x28, 0x73, 0x82, 0x33, 0x20, 0x8e, 0xfe, 0xe2, 0x39, 0x38, 0xe4, 0xe7, 0xf2, 0xa0, 0xa3, 0xb9, 85 | 0x76, 0x07, 0xc5, 0x36, 0x51, 0xe0, 0x57, 0x1a, 0x49, 0xf4, 0x61, 0xc6, 0x1f, 0x48, 0xf4, 0x70, 86 | 0x29, 0xa1, 0x2e, 0xfb, 0xba, 0xfd, 0x3f, 0xb0, 0xd0, 0x76, 0xdb, 0x18, 0x7c, 0x63, 0xed, 0xa1, 87 | 0xe4, 0xb5, 0x50, 0xb5, 0x43, 0xa8, 0x5d, 0x49, 0xf2, 0xa4, 0x07, 0x96, 0xf6, 0x40, 0xfc, 0xef, 88 | 0x9c, 0xc8, 0x2c, 0xe1, 0xd0, 0x70, 0xcd, 0x87, 0x94, 0x24, 0xea, 0xfa, 0xf5, 0x56, 0x39, 0xeb, 89 | 0x22, 0xfa, 0x64, 0x54, 0x4b, 0x9d, 0x40, 0xb0, 0x83, 0x5b, 0xfa, 0xb5, 0x44, 0x8e, 0x6b, 0x48, 90 | 0x7e, 0xfa, 0x49, 0xee, 0x9a, 0x82, 0x73, 0x7f, 0x25, 0xb1, 0x0e, 0x06, 0x43, 0xf4, 0xaa, 0xd4, 91 | 0x92, 0x72, 0x7f, 0xf2, 0xb5, 0x8f, 0x4b, 0xac, 0x9b, 0x24, 0xaf, 0x28, 0xee, 0x48, 0xd4, 0x39, 92 | 0x68, 0x8f, 0x59, 0x61, 0x2c, 0xaf, 0x93, 0x0b, 0xb2, 0x86, 0xa2, 0x3e, 0x21, 0xdb, 0x78, 0x7e, 93 | 0x9e, 0xdb, 0xcc, 0x46, 0xb9, 0x97, 0x49, 0x0c, 0x2c, 0x32, 0xab, 0x3d, 0x39, 0xab, 0x44, 0x7b, 94 | 0x7c, 0xaf, 0xf3, 0x32, 0x0d, 0xcc, 0x5b, 0xad, 0x42, 0x57, 0xf2, 0x0d, 0x2f, 0x1b, 0xe3, 0xbf, 95 | 0xf2, 0xe3, 0xe7, 0xb4, 0x9a, 0x29, 0x37, 0x78, 0x4a, 0x11, 0xcd, 0x9f, 0xcc, 0x6e, 0xbb, 0xdc, 96 | 0x45, 0xb0, 0xdd, 0x0b, 0x83, 0xc0, 0xc0, 0x0d, 0x51, 0xbc, 0xbb, 0x75, 0x12, 0x6a, 0x85, 0xba, 97 | 0x71, 0x80, 0x5d, 0x9b, 0x6c, 0xa4, 0x93, 0xf4, 0xae, 0xba, 0x28, 0x82, 0xd0, 0x56, 0x79, 0xdc, 98 | 0x39, 0x6d, 0xbc, 0x49, 0x62, 0x7d, 0x51, 0x60, 0xaa, 0x8d, 0x01, 0xd9, 0x15, 0xd0, 0x9c, 0xf9, 99 | 0x36, 0x5f, 0x82, 0x1f, 0x2a, 0xfc, 0xd6, 0xc1, 0x18, 0x87, 0xd8, 0x89, 0x49, 0x75, 0x29, 0xfe, 100 | 0xbc, 0x35, 0x37, 0x54, 0xec, 0x0e, 0x41, 0x9a, 0xec, 0x45, 0x37, 0xf7, 0x46, 0xbd, 0x17, 0x06, 101 | 0xb3, 0xf1, 0xc7, 0x70, 0x9a, 0x2b, 0x5a, 0x13, 0x3a, 0x58, 0xfc, 0xb3, 0x2b, 0xd0, 0x16, 0x07, 102 | 0x47, 0xc1, 0xd1, 0x4b, 0x7d, 0x77, 0x17, 0xd9, 0x34, 0x5a, 0x09, 0xd2, 0x8c, 0xfc, 0x6e, 0x39, 103 | 0x59>> 104 | 105 | test "Can validate JWPlayer handshake" do 106 | c0_and_c1 = @jwplayer_c0 <> @jwplayer_c1 107 | 108 | assert DigestHandshakeFormat.is_valid_format(c0_and_c1) == :yes 109 | end 110 | 111 | test "No unparsed binary after reading c0, c1, and c2" do 112 | c0_and_c1 = @jwplayer_c0 <> @jwplayer_c1 113 | 114 | state = DigestHandshakeFormat.new() 115 | {state, _} = DigestHandshakeFormat.process_bytes(state, c0_and_c1) 116 | {state, _} = DigestHandshakeFormat.process_bytes(state, @jwplayer_c1) 117 | 118 | assert state.unparsed_binary == <<>> 119 | end 120 | 121 | test "Can recognize its own c0 and c1 as valid" do 122 | {_, binary} = DigestHandshakeFormat.new() |> DigestHandshakeFormat.create_p0_and_p1_to_send() 123 | 124 | assert :yes == DigestHandshakeFormat.is_valid_format(binary) 125 | end 126 | end -------------------------------------------------------------------------------- /apps/rtmp/test/handshake/general_handshake_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Handshake.GeneralHandshakeTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | 5 | test "Initial creation of handshake with old format specified returns packet 0" do 6 | assert { 7 | _, 8 | %Rtmp.Handshake.ParseResult{ 9 | current_state: :waiting_for_data, 10 | bytes_to_send: <<3::8, _::binary>>} 11 | } 12 | = Rtmp.Handshake.new(:old) 13 | end 14 | 15 | test "Initial creation of handshake with old format specified returns packet 1" do 16 | assert {_, %Rtmp.Handshake.ParseResult{ 17 | current_state: :waiting_for_data, 18 | bytes_to_send: <<_::8, _::4 * 8, 0::4 * 8, _::1528 * 8>>} 19 | } 20 | = Rtmp.Handshake.new(:old) 21 | end 22 | 23 | test "Can parse full old format handshake" do 24 | assert {handshake, %Rtmp.Handshake.ParseResult{ 25 | current_state: :waiting_for_data, 26 | bytes_to_send: <<3::1 * 8, time::4 * 8, 0::4 * 8, random::1528 * 8>> 27 | }} = Rtmp.Handshake.new(:old) 28 | 29 | # send packet 0 30 | assert {handshake, %Rtmp.Handshake.ParseResult{ 31 | current_state: :waiting_for_data, 32 | bytes_to_send: <<>> 33 | }} = Rtmp.Handshake.process_bytes(handshake, <<3>>) 34 | 35 | # send packet 1 36 | assert {handshake, %Rtmp.Handshake.ParseResult{ 37 | current_state: :waiting_for_data, 38 | bytes_to_send: <<1::4 * 8, 0::4 * 8, 555::1528 * 8>> # their p2 should match my p1 39 | }} = Rtmp.Handshake.process_bytes(handshake, <<1::4 * 8, 0::4 * 8, 555::1528 * 8>>) 40 | 41 | # send packet 2 42 | assert {handshake, %Rtmp.Handshake.ParseResult{ 43 | current_state: :success, 44 | bytes_to_send: <<>> 45 | }} = Rtmp.Handshake.process_bytes(handshake, <>) 46 | 47 | # final result 48 | assert {_, %Rtmp.Handshake.HandshakeResult{ 49 | peer_start_timestamp: 1, 50 | remaining_binary: <<>> 51 | }} = Rtmp.Handshake.get_handshake_result(handshake) 52 | end 53 | 54 | test "Two old handshake instances can complete handshake against each other" do 55 | {handshake1, %Rtmp.Handshake.ParseResult{bytes_to_send: bytes1_1}} = Rtmp.Handshake.new(:old) 56 | {handshake2, %Rtmp.Handshake.ParseResult{bytes_to_send: bytes2_1}} = Rtmp.Handshake.new(:old) 57 | 58 | # packets 0 and 1 59 | assert {handshake1, %Rtmp.Handshake.ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes1_2}} 60 | = Rtmp.Handshake.process_bytes(handshake1, bytes2_1) 61 | 62 | assert {handshake2, %Rtmp.Handshake.ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes2_2}} 63 | = Rtmp.Handshake.process_bytes(handshake2, bytes1_1) 64 | 65 | # packet 2 66 | assert {_, %Rtmp.Handshake.ParseResult{current_state: :success}} 67 | = Rtmp.Handshake.process_bytes(handshake1, bytes2_2) 68 | 69 | assert {_, %Rtmp.Handshake.ParseResult{current_state: :success}} 70 | = Rtmp.Handshake.process_bytes(handshake2, bytes1_2) 71 | end 72 | 73 | test "Handshakes with unknown format specified do not send p0 and p1 until they receive p0 and p1" do 74 | assert {handshake, %Rtmp.Handshake.ParseResult{ 75 | current_state: :waiting_for_data, 76 | bytes_to_send: <<>> 77 | }} = Rtmp.Handshake.new(:unknown) 78 | 79 | assert {handshake, %Rtmp.Handshake.ParseResult{ 80 | current_state: :waiting_for_data, 81 | bytes_to_send: <<>> 82 | }} = Rtmp.Handshake.process_bytes(handshake, <<3>>) 83 | 84 | assert {_, %Rtmp.Handshake.ParseResult{ 85 | current_state: :waiting_for_data, 86 | bytes_to_send: <<3::1 * 8, _::4 * 8, 0::4 * 8, _::1528 * 8, _::binary>> 87 | }} = Rtmp.Handshake.process_bytes(handshake, <<1::4 * 8, 0::4 * 8, 555::1528 * 8>>) 88 | end 89 | 90 | test "Two digest handshake instances can complete handshake against each other" do 91 | {client, %Rtmp.Handshake.ParseResult{bytes_to_send: c0_c1}} = Rtmp.Handshake.new(:digest) 92 | {server, %Rtmp.Handshake.ParseResult{bytes_to_send: <<>>}} = Rtmp.Handshake.new(:unknown) 93 | 94 | assert {server, %Rtmp.Handshake.ParseResult{current_state: :waiting_for_data, bytes_to_send: s0_s1_s2}} 95 | = Rtmp.Handshake.process_bytes(server, c0_c1) 96 | 97 | assert {_client, %Rtmp.Handshake.ParseResult{current_state: :success, bytes_to_send: c2}} 98 | = Rtmp.Handshake.process_bytes(client, s0_s1_s2) 99 | 100 | assert {_, %Rtmp.Handshake.ParseResult{current_state: :success}} 101 | = Rtmp.Handshake.process_bytes(server, c2) 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /apps/rtmp/test/protocol/chunk_io_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.ChunkIoTest do 2 | use ExUnit.Case, async: true 3 | alias Rtmp.Protocol.RawMessage, as: RawMessage 4 | alias Rtmp.Protocol.ChunkIo, as: ChunkIo 5 | 6 | @previous_chunk_0_binary <<0::2, 50::6, 100::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 7 | @previous_chunk_1_binary <<1::2, 50::6, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 152::size(100)-unit(8)>> 8 | 9 | describe "Deserialization" do 10 | 11 | test "Can read full type 0 chunk with small chunk stream id" do 12 | binary = <<0::2, 50::6, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 13 | result = ChunkIo.new() |> ChunkIo.deserialize(binary) 14 | 15 | assert {_, %RawMessage{ 16 | timestamp: 72, 17 | message_type_id: 3, 18 | stream_id: 55, 19 | payload: <<152::100 * 8>> 20 | }} = result 21 | end 22 | 23 | test "Can read full type 0 chunk with medium chunk stream id" do 24 | binary = <<0::2, 0::6, 200::8, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 25 | result = ChunkIo.new() |> ChunkIo.deserialize(binary) 26 | 27 | assert {_, %RawMessage{ 28 | timestamp: 72, 29 | message_type_id: 3, 30 | stream_id: 55, 31 | payload: <<152::100 * 8>> 32 | }} = result 33 | end 34 | 35 | test "Can read full type 0 chunk with large chunk stream id" do 36 | binary = <<0::2, 1::6, 60001::16, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 37 | result = ChunkIo.new() |> ChunkIo.deserialize(binary) 38 | 39 | assert {_, %RawMessage{ 40 | timestamp: 72, 41 | message_type_id: 3, 42 | stream_id: 55, 43 | payload: <<152::100 * 8>> 44 | }} = result 45 | end 46 | 47 | test "Can read full type 1 chunk" do 48 | binary = <<1::2, 50::6, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 152::size(100)-unit(8)>> 49 | 50 | assert {io, %RawMessage{}} = ChunkIo.new() |> ChunkIo.deserialize(@previous_chunk_0_binary) 51 | assert {_, %RawMessage{ 52 | timestamp: 172, 53 | message_type_id: 3, 54 | stream_id: 55, 55 | payload: <<152::100 * 8>> 56 | }} = ChunkIo.deserialize(io, binary) 57 | end 58 | 59 | test "Can read full type 2 chunk" do 60 | binary = <<2::2, 50::6, 72::size(3)-unit(8), 152::size(100)-unit(8)>> 61 | 62 | assert {io, %RawMessage{}} = ChunkIo.new() |> ChunkIo.deserialize(@previous_chunk_0_binary) 63 | assert {_, %RawMessage{ 64 | timestamp: 172, 65 | message_type_id: 3, 66 | stream_id: 55, 67 | payload: <<152::100 * 8>> 68 | }} = ChunkIo.deserialize(io, binary) 69 | end 70 | 71 | test "Can read full type 3 chunk" do 72 | binary = <<3::2, 50::6, 152::size(100)-unit(8)>> 73 | 74 | assert {io, %RawMessage{}} = ChunkIo.new() |> ChunkIo.deserialize(@previous_chunk_0_binary) 75 | assert {io, %RawMessage{}} = ChunkIo.deserialize(io, @previous_chunk_1_binary) 76 | assert {_, %RawMessage{ 77 | timestamp: 244, 78 | message_type_id: 3, 79 | stream_id: 55, 80 | payload: <<152::100 * 8>> 81 | }} = ChunkIo.deserialize(io, binary) 82 | end 83 | 84 | test "Can read full type 0 chunk with extended timestamp" do 85 | binary = <<0::2, 50::6, 16777215::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 16777216::size(4)-unit(8), 152::size(100)-unit(8)>> 86 | result = ChunkIo.new() |> ChunkIo.deserialize(binary) 87 | 88 | assert {_, %RawMessage{ 89 | timestamp: 16777216, 90 | message_type_id: 3, 91 | stream_id: 55, 92 | payload: <<152::100 * 8>> 93 | }} = result 94 | end 95 | 96 | test "Can read full type 1 chunk with extended timestamp" do 97 | binary = <<1::2, 50::6, 16777215::size(3)-unit(8), 100::size(3)-unit(8), 3::8, 16777216::size(4)-unit(8), 152::size(100)-unit(8)>> 98 | 99 | assert {io, %RawMessage{}} = ChunkIo.new() |> ChunkIo.deserialize(@previous_chunk_0_binary) 100 | assert {_, %RawMessage{ 101 | timestamp: 16777316, 102 | message_type_id: 3, 103 | stream_id: 55, 104 | payload: <<152::100 * 8>> 105 | }} = ChunkIo.deserialize(io, binary) 106 | end 107 | 108 | test "Can read full type 2 chunk with extended timestamp" do 109 | binary = <<2::2, 50::6, 16777215::size(3)-unit(8), 16777216::size(4)-unit(8), 152::size(100)-unit(8)>> 110 | 111 | assert {io, %RawMessage{}} = ChunkIo.new() |> ChunkIo.deserialize(@previous_chunk_0_binary) 112 | assert {_, %RawMessage{ 113 | timestamp: 16777316, 114 | message_type_id: 3, 115 | stream_id: 55, 116 | payload: <<152::100 * 8>> 117 | }} = ChunkIo.deserialize(io, binary) 118 | end 119 | 120 | test "Incomplete chunk returns incomplete notification" do 121 | binary = <<0::2, 50::6, 100::size(3)-unit(8), 100::size(3)-unit(8)>> 122 | 123 | assert {_, :incomplete} = ChunkIo.new() |> ChunkIo.deserialize(binary) 124 | end 125 | 126 | test "Can read message spread across multiple deserialization calls" do 127 | binary1 = <<0::2, 50::6, 72::size(3)-unit(8), 100::size(3)-unit(8), 3::8>> 128 | binary2 = <<55::size(4)-unit(8)-little, 0::size(90)-unit(8)>> 129 | binary3 = <<152::size(10)-unit(8)>> 130 | 131 | io = ChunkIo.new() 132 | assert {io, :incomplete} = ChunkIo.deserialize(io, binary1) 133 | assert {io, :incomplete} = ChunkIo.deserialize(io, binary2) 134 | assert {_, %RawMessage{ 135 | timestamp: 72, 136 | message_type_id: 3, 137 | stream_id: 55, 138 | payload: <<152::100 * 8>> 139 | }} = ChunkIo.deserialize(io, binary3) 140 | end 141 | 142 | test "Can read message exceeding maximum chunk size" do 143 | binary1 = <<0::2, 50::6, 72::size(3)-unit(8), 138::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 0::size(128)-unit(8)>> 144 | binary2 = <<3::2, 50::6, 152::10 * 8>> 145 | assert {io, :split_message} = ChunkIo.new() |> ChunkIo.deserialize(binary1) 146 | 147 | assert {_, %RawMessage{ 148 | timestamp: 72, 149 | message_type_id: 3, 150 | stream_id: 55, 151 | payload: <<152::138 * 8>> 152 | }} = ChunkIo.deserialize(io, binary2) 153 | end 154 | 155 | test "Can change receiving maximum chunk size" do 156 | binary = <<0::2, 50::6, 72::size(3)-unit(8), 200::size(3)-unit(8), 3::8, 55::size(4)-unit(8)-little, 152::size(200)-unit(8)>> 157 | result = 158 | ChunkIo.new() 159 | |> ChunkIo.set_receiving_max_chunk_size(201) 160 | |> ChunkIo.deserialize(binary) 161 | 162 | assert {_, %RawMessage{ 163 | timestamp: 72, 164 | message_type_id: 3, 165 | stream_id: 55, 166 | payload: <<152::200 * 8>> 167 | }} = result 168 | end 169 | end 170 | 171 | describe "Serialization" do 172 | 173 | test "Serialize: Initial chunk for csid" do 174 | message = %RawMessage{ 175 | timestamp: 72, 176 | message_type_id: 3, 177 | stream_id: 55, 178 | payload: <<152::size(100)-unit(8)>> 179 | } 180 | 181 | {_, binary} = ChunkIo.new() |> ChunkIo.serialize(message, 50) 182 | 183 | expected_binary = <<0::2, 50::6, 72::size(3)-unit(8), 100::size(3)-unit(8), 184 | 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 185 | 186 | assert expected_binary == binary 187 | end 188 | 189 | test "Serialize: 2nd chunk, same sid, different message length" do 190 | message1 = %RawMessage{ 191 | timestamp: 72, 192 | message_type_id: 3, 193 | stream_id: 55, 194 | payload: <<152::size(100)-unit(8)>> 195 | } 196 | 197 | message2 = %RawMessage{ 198 | timestamp: 82, 199 | message_type_id: 3, 200 | stream_id: 55, 201 | payload: <<152::size(101)-unit(8)>> 202 | } 203 | 204 | {serializer, _} = ChunkIo.new() |> ChunkIo.serialize(message1, 50) 205 | {_, binary} = ChunkIo.serialize(serializer, message2, 50) 206 | 207 | expected_binary = <<1::2, 50::6, 10::size(3)-unit(8), 101::size(3)-unit(8), 208 | 3::8, 152::size(101)-unit(8)>> 209 | 210 | assert expected_binary == binary 211 | end 212 | 213 | test "Serialize: 2nd chunk, same sid, message length, and type id" do 214 | message1 = %RawMessage{ 215 | timestamp: 72, 216 | message_type_id: 3, 217 | stream_id: 55, 218 | payload: <<152::size(100)-unit(8)>> 219 | } 220 | 221 | message2 = %RawMessage{ 222 | timestamp: 82, 223 | message_type_id: 3, 224 | stream_id: 55, 225 | payload: <<152::size(100)-unit(8)>> 226 | } 227 | 228 | {serializer, _} = ChunkIo.new() |> ChunkIo.serialize(message1, 50) 229 | {_, binary} = ChunkIo.serialize(serializer, message2, 50) 230 | 231 | expected_binary = <<2::2, 50::6, 10::size(3)-unit(8), 152::size(100)-unit(8)>> 232 | 233 | assert expected_binary == binary 234 | end 235 | 236 | test "Serialize: 3rd chunk, same sid, length, typeid, and timestamp delta" do 237 | message1 = %RawMessage{ 238 | timestamp: 72, 239 | message_type_id: 3, 240 | stream_id: 55, 241 | payload: <<152::size(100)-unit(8)>> 242 | } 243 | 244 | message2 = %RawMessage{ 245 | timestamp: 82, 246 | message_type_id: 3, 247 | stream_id: 55, 248 | payload: <<152::size(100)-unit(8)>> 249 | } 250 | 251 | message3 = %RawMessage{ 252 | timestamp: 92, 253 | message_type_id: 3, 254 | stream_id: 55, 255 | payload: <<152::size(100)-unit(8)>> 256 | } 257 | 258 | {serializer, _} = ChunkIo.new() |> ChunkIo.serialize(message1, 50) 259 | {serializer, _} = ChunkIo.serialize(serializer, message2, 50) 260 | {_, binary} = ChunkIo.serialize(serializer, message3, 50) 261 | 262 | expected_binary = <<3::2, 50::6, 152::size(100)-unit(8)>> 263 | assert expected_binary == binary 264 | end 265 | 266 | test "Serialize: Messages larger than max chunk size are split" do 267 | message = %RawMessage{ 268 | timestamp: 72, 269 | message_type_id: 3, 270 | stream_id: 55, 271 | payload: <<152::size(101)-unit(8)>> 272 | } 273 | 274 | {_, binary} = 275 | ChunkIo.new() 276 | |> ChunkIo.set_sending_max_chunk_size(100) 277 | |> ChunkIo.serialize(message, 50) 278 | 279 | expected_binary = <<0::2, 50::6, 72::size(3)-unit(8), 101::size(3)-unit(8), 280 | 3::8, 55::size(4)-unit(8)-little, 0::size(100)-unit(8), 281 | 3::2, 50::6, 152::size(1)-unit(8)>> 282 | 283 | assert expected_binary == binary 284 | end 285 | 286 | test "Serialize: Can force no compression for messages marked as such" do 287 | message1 = %RawMessage{ 288 | timestamp: 72, 289 | message_type_id: 3, 290 | stream_id: 55, 291 | payload: <<152::size(100)-unit(8)>> 292 | } 293 | 294 | message2 = %RawMessage{ 295 | timestamp: 82, 296 | message_type_id: 3, 297 | stream_id: 55, 298 | force_uncompressed: true, 299 | payload: <<152::size(100)-unit(8)>> 300 | } 301 | 302 | {serializer, _} = ChunkIo.new() |> ChunkIo.serialize(message1, 50) 303 | {_, binary} = ChunkIo.serialize(serializer, message2, 50) 304 | 305 | expected_binary = <<0::2, 50::6, 82::size(3)-unit(8), 100::size(3)-unit(8), 306 | 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 307 | 308 | assert expected_binary == binary 309 | end 310 | 311 | test "Serialize: 2nd chunk with negative time serialized as type 0 chunk" do 312 | message1 = %RawMessage{ 313 | timestamp: 72, 314 | message_type_id: 3, 315 | stream_id: 55, 316 | payload: <<152::size(100)-unit(8)>> 317 | } 318 | 319 | message2 = %RawMessage{ 320 | timestamp: 62, 321 | message_type_id: 3, 322 | stream_id: 55, 323 | payload: <<152::size(100)-unit(8)>> 324 | } 325 | 326 | {serializer, _} = ChunkIo.new() |> ChunkIo.serialize(message1, 50) 327 | {_, binary} = ChunkIo.serialize(serializer, message2, 50) 328 | 329 | expected_binary = <<0::2, 50::6, 62::size(3)-unit(8), 100::size(3)-unit(8), 330 | 3::8, 55::size(4)-unit(8)-little, 152::size(100)-unit(8)>> 331 | 332 | assert expected_binary == binary 333 | end 334 | 335 | end 336 | end -------------------------------------------------------------------------------- /apps/rtmp/test/protocol/handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.HandlerTest do 2 | use ExUnit.Case, async: false 3 | require Logger 4 | 5 | alias Rtmp.Protocol.Handler, as: ProtocolHandler 6 | alias Rtmp.Protocol.DetailedMessage, as: DetailedMessage 7 | alias Rtmp.Protocol.Messages.VideoData, as: VideoData 8 | alias Rtmp.Protocol.Messages.AudioData, as: AudioData 9 | alias Rtmp.Protocol.Messages.SetChunkSize, as: SetChunkSize 10 | 11 | def send_data(pid, data, packet_type) do 12 | _ = send(pid, {:binary, data, packet_type}) 13 | :ok 14 | end 15 | 16 | def handle_rtmp_input(pid, message) do 17 | _ = send(pid, {:message, message}) 18 | :ok 19 | end 20 | 21 | def notify_byte_count(pid, in_or_out, count) do 22 | _ = send(pid, {in_or_out, count}) 23 | :ok 24 | end 25 | 26 | test "Valid chunk 0 RTMP binary input deserialized and sent via session function" do 27 | input = <<0::2, 50::6, 72::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 152::size(12)-unit(8)>> 28 | 29 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 30 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 31 | assert :ok = ProtocolHandler.notify_input(handler, input) 32 | 33 | assert_receive {:message, %DetailedMessage{ 34 | timestamp: 72, 35 | stream_id: 55, 36 | content: %VideoData{ 37 | data: <<152::12 * 8>> 38 | } 39 | }} 40 | end 41 | 42 | test "Can deserialize compressed (type 2) binary imput" do 43 | input1 = <<0::2, 50::6, 72::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 152::size(12)-unit(8)>> 44 | input2 = <<1::2, 50::6, 10::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 122::size(12)-unit(8)>> 45 | 46 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 47 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 48 | assert :ok = ProtocolHandler.notify_input(handler, input1) 49 | assert :ok = ProtocolHandler.notify_input(handler, input2) 50 | 51 | assert_receive {:message, %DetailedMessage{ 52 | timestamp: 82, 53 | stream_id: 55, 54 | content: %VideoData{ 55 | data: <<122::12 * 8>> 56 | } 57 | }} 58 | end 59 | 60 | test "Split chunks are read and passed on properly" do 61 | input1 = <<0::2, 50::6, 72::size(3)-unit(8), 138::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 0::size(128)-unit(8)>> 62 | input2 = <<3::2, 50::6, 122::size(10)-unit(8)>> 63 | 64 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 65 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 66 | assert :ok = ProtocolHandler.notify_input(handler, input1) 67 | assert :ok = ProtocolHandler.notify_input(handler, input2) 68 | 69 | assert_receive {:message, %DetailedMessage{ 70 | timestamp: 72, 71 | stream_id: 55, 72 | content: %VideoData{ 73 | data: <<122::138 * 8>> 74 | } 75 | }} 76 | end 77 | 78 | test "Automatically adjusts to SetChunkSize messages" do 79 | input1 = <<0::2, 50::6, 72::size(3)-unit(8), 4::size(3)-unit(8), 1::8, 55::size(4)-unit(8)-little, 200::size(4)-unit(8)>> 80 | input2 = <<1::2, 50::6, 10::size(3)-unit(8), 200::size(3)-unit(8), 9::8, 122::size(200)-unit(8)>> 81 | 82 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 83 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 84 | assert :ok = ProtocolHandler.notify_input(handler, input1) 85 | assert :ok = ProtocolHandler.notify_input(handler, input2) 86 | 87 | assert_receive {:message, %DetailedMessage{ 88 | timestamp: 82, 89 | stream_id: 55, 90 | content: %VideoData{ 91 | data: <<122::200 * 8>> 92 | } 93 | }} 94 | end 95 | 96 | test "Passed in messages are serialized (with compression) and sent to socket function" do 97 | input1 = %DetailedMessage{ 98 | timestamp: 72, 99 | stream_id: 55, 100 | content: %VideoData{data: <<152::12 * 8>>} 101 | } 102 | 103 | input2 = %DetailedMessage{ 104 | timestamp: 82, 105 | stream_id: 55, 106 | content: %VideoData{ 107 | data: <<122::13 * 8>> 108 | } 109 | } 110 | 111 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 112 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 113 | assert :ok = ProtocolHandler.send_message(handler, input1) 114 | assert :ok = ProtocolHandler.send_message(handler, input2) 115 | 116 | expected_binary1 = <<0::2, 21::6, 72::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 152::size(12)-unit(8)>> 117 | expected_binary2 = <<1::2, 21::6, 10::size(3)-unit(8), 13::size(3)-unit(8), 9::8, 122::size(13)-unit(8)>> 118 | 119 | assert_receive {:binary, ^expected_binary1, _} 120 | assert_receive {:binary, ^expected_binary2, _} 121 | end 122 | 123 | test "Passed in message is split if greater than max chunk size" do 124 | input1 = %DetailedMessage{ 125 | timestamp: 72, 126 | stream_id: 55, 127 | content: %VideoData{data: <<122::138 * 8>>} 128 | } 129 | 130 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 131 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 132 | assert :ok = ProtocolHandler.send_message(handler, input1) 133 | 134 | expected_binary1 = <<0::2, 21::6, 72::size(3)-unit(8), 138::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 0::size(128)-unit(8)>> 135 | expected_binary2 = <<3::2, 21::6, 122::size(10)-unit(8)>> 136 | expected_binary = expected_binary1 <> expected_binary2 137 | 138 | assert_receive {:binary, ^expected_binary, _} 139 | end 140 | 141 | test "Sending a SetChunkSize message updates the chunk size for following chunks" do 142 | input1 = %DetailedMessage{ 143 | timestamp: 72, 144 | stream_id: 55, 145 | content: %SetChunkSize{size: 200} 146 | } 147 | 148 | input2 = %DetailedMessage{ 149 | timestamp: 82, 150 | stream_id: 55, 151 | content: %VideoData{ 152 | data: <<122::200 * 8>> 153 | } 154 | } 155 | 156 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 157 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 158 | assert :ok = ProtocolHandler.send_message(handler, input1) 159 | assert :ok = ProtocolHandler.send_message(handler, input2) 160 | 161 | expected_binary1 = <<0::2, 2::6, 72::size(3)-unit(8), 4::size(3)-unit(8), 1::8, 55::size(4)-unit(8)-little, 200::size(4)-unit(8)>> 162 | expected_binary2 = <<0::2, 21::6, 82::size(3)-unit(8), 200::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 122::size(200)-unit(8)>> 163 | 164 | assert_receive {:binary, ^expected_binary1, _} 165 | assert_receive {:binary, ^expected_binary2, _} 166 | end 167 | 168 | test "Can read multiple chunks in a single packet" do 169 | input1 = <<0::2, 50::6, 72::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 152::size(12)-unit(8)>> 170 | input2 = <<1::2, 50::6, 10::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 122::size(12)-unit(8)>> 171 | 172 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 173 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 174 | assert :ok = ProtocolHandler.notify_input(handler, input1 <> input2) 175 | 176 | assert_receive {:message, %DetailedMessage{ 177 | timestamp: 72, 178 | stream_id: 55, 179 | content: %VideoData{ 180 | data: <<152::12 * 8>> 181 | } 182 | }} 183 | 184 | assert_receive {:message, %DetailedMessage{ 185 | timestamp: 82, 186 | stream_id: 55, 187 | content: %VideoData{ 188 | data: <<122::12 * 8>> 189 | } 190 | }} 191 | end 192 | 193 | test "Announces total bytes received after processing input" do 194 | input1 = <<0::2, 50::6, 72::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 152::size(12)-unit(8)>> 195 | input2 = <<1::2, 50::6, 10::size(3)-unit(8), 12::size(3)-unit(8), 9::8, 122::size(12)-unit(8)>> 196 | 197 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 198 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 199 | assert :ok = ProtocolHandler.notify_input(handler, input1) 200 | assert :ok = ProtocolHandler.notify_input(handler, input2) 201 | 202 | expected_receive_count = byte_size(input1 <> input2) 203 | assert_receive {:bytes_received, ^expected_receive_count}, 1000 204 | end 205 | 206 | test "Announces total bytes sent after sending bytes to socket handler" do 207 | input1 = %DetailedMessage{ 208 | timestamp: 72, 209 | stream_id: 55, 210 | content: %SetChunkSize{size: 200} 211 | } 212 | 213 | input2 = %DetailedMessage{ 214 | timestamp: 82, 215 | stream_id: 55, 216 | content: %VideoData{ 217 | data: <<122::200 * 8>> 218 | } 219 | } 220 | 221 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 222 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 223 | assert :ok = ProtocolHandler.send_message(handler, input1) 224 | assert :ok = ProtocolHandler.send_message(handler, input2) 225 | 226 | expected_binary1 = <<0::2, 2::6, 72::size(3)-unit(8), 4::size(3)-unit(8), 1::8, 55::size(4)-unit(8)-little, 200::size(4)-unit(8)>> 227 | expected_binary2 = <<0::2, 21::6, 82::size(3)-unit(8), 200::size(3)-unit(8), 9::8, 55::size(4)-unit(8)-little, 122::size(200)-unit(8)>> 228 | expected_sent_size = byte_size(expected_binary1 <> expected_binary2) 229 | 230 | assert_receive {:bytes_sent, ^expected_sent_size}, 1000 231 | end 232 | 233 | test "Video packets passed to socket handler are flagged as video packet type" do 234 | input = %DetailedMessage{ 235 | timestamp: 72, 236 | stream_id: 55, 237 | content: %VideoData{data: <<5::200>>} 238 | } 239 | 240 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 241 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 242 | assert :ok = ProtocolHandler.send_message(handler, input) 243 | 244 | assert_receive {:binary, _, :video} 245 | end 246 | 247 | test "Audio packets passed to socket handler are flagged as audio packet type" do 248 | input = %DetailedMessage{ 249 | timestamp: 72, 250 | stream_id: 55, 251 | content: %AudioData{data: <<6::200>>} 252 | } 253 | 254 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 255 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 256 | assert :ok = ProtocolHandler.send_message(handler, input) 257 | 258 | assert_receive {:binary, _, :audio} 259 | end 260 | 261 | test "Misc rtmp messages are passed to socket handler are flagged as misc packet type" do 262 | input = %DetailedMessage{ 263 | timestamp: 72, 264 | stream_id: 55, 265 | content: %SetChunkSize{size: 200} 266 | } 267 | 268 | assert {:ok, handler} = ProtocolHandler.start_link("id", self(), __MODULE__) 269 | assert :ok = ProtocolHandler.set_session(handler, self(), __MODULE__) 270 | assert :ok = ProtocolHandler.send_message(handler, input) 271 | 272 | assert_receive {:binary, _, :misc} 273 | end 274 | end -------------------------------------------------------------------------------- /apps/rtmp/test/protocol/raw_message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.RawMessageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Rtmp.Protocol.RawMessage, as: RawMessage 5 | alias Rtmp.Protocol.DetailedMessage, as: RtmpDetailedMessage 6 | 7 | test "Can unpack SetChunkSize message" do 8 | rtmp_message = %RawMessage{message_type_id: 1, payload: <<4000::32>>} 9 | {:ok, message} = RawMessage.unpack(rtmp_message) 10 | 11 | assert %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.SetChunkSize{size: 4000}} = message 12 | end 13 | 14 | test "Can unpack Abort message" do 15 | rtmp_message = %RawMessage{message_type_id: 2, payload: <<500::32>>} 16 | {:ok, message} = RawMessage.unpack(rtmp_message) 17 | 18 | assert %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Abort{stream_id: 500}} = message 19 | end 20 | 21 | test "Can unpack Acknowledgement message" do 22 | rtmp_message = %RawMessage{message_type_id: 3, payload: <<25::32>>} 23 | {:ok, message} = RawMessage.unpack(rtmp_message) 24 | 25 | assert %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Acknowledgement{sequence_number: 25}} = message 26 | end 27 | 28 | test "Can unpack Window Acknowlegement Size message" do 29 | rtmp_message = %RawMessage{message_type_id: 5, payload: <<26::32>>} 30 | {:ok, message} = RawMessage.unpack(rtmp_message) 31 | 32 | assert %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.WindowAcknowledgementSize{size: 26}} = message 33 | end 34 | 35 | test "Can unpack Set Peer Bandwidth message" do 36 | rtmp_message = %RawMessage{message_type_id: 6, payload: <<20::32, 1::8>>} 37 | {:ok, message} = RawMessage.unpack(rtmp_message) 38 | 39 | assert %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.SetPeerBandwidth{window_size: 20, limit_type: :soft}} = message 40 | end 41 | 42 | test "Can unpack User Control Stream Begin" do 43 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 44 | type: :stream_begin, 45 | stream_id: 521, 46 | buffer_length: nil, 47 | timestamp: nil 48 | }} 49 | 50 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<0::16, 521::32>>} 51 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 52 | end 53 | 54 | test "Can unpack User Control Stream EOF" do 55 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 56 | type: :stream_eof, 57 | stream_id: 555, 58 | buffer_length: nil, 59 | timestamp: nil 60 | }} 61 | 62 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<1::16, 555::32>>} 63 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 64 | end 65 | 66 | test "Can unpack User Control Stream Dry" do 67 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 68 | type: :stream_dry, 69 | stream_id: 666, 70 | buffer_length: nil, 71 | timestamp: nil 72 | }} 73 | 74 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<2::16, 666::32>>} 75 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 76 | end 77 | 78 | test "Can unpack User Control Set Buffer Length" do 79 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 80 | type: :set_buffer_length, 81 | stream_id: 500, 82 | buffer_length: 300, 83 | timestamp: nil 84 | }} 85 | 86 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<3::16, 500::32, 300::32>>} 87 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 88 | end 89 | 90 | test "Can unpack User Control Stream Is Recorded" do 91 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 92 | type: :stream_is_recorded, 93 | stream_id: 333, 94 | buffer_length: nil, 95 | timestamp: nil 96 | }} 97 | 98 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<4::16, 333::32>>} 99 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 100 | end 101 | 102 | test "Can unpack User Control Ping Request" do 103 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 104 | type: :ping_request, 105 | stream_id: nil, 106 | buffer_length: nil, 107 | timestamp: 999 108 | }} 109 | 110 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<6::16, 999::32>>} 111 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 112 | end 113 | 114 | test "Can unpack User Control Ping Response" do 115 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 116 | type: :ping_response, 117 | stream_id: nil, 118 | buffer_length: nil, 119 | timestamp: 900 120 | }} 121 | 122 | rtmp_message = %RawMessage{message_type_id: 4, payload: <<7::16, 900::32>>} 123 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 124 | end 125 | 126 | test "Can unpack amf 0 encoded command message" do 127 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Amf0Command{ 128 | command_name: "something", 129 | transaction_id: 1221.0, 130 | command_object: nil, 131 | additional_values: ["test"] 132 | }} 133 | 134 | amf_objects = ["something", 1221.0, nil, "test"] 135 | binary = Amf0.serialize(amf_objects) 136 | 137 | rtmp_message = %RawMessage{message_type_id: 20, payload: binary} 138 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 139 | end 140 | 141 | test "Can unpack amf0 encoded data message" do 142 | amf_objects = ["something", 1221.0, nil, "test"] 143 | 144 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Amf0Data{parameters: amf_objects}} 145 | binary = Amf0.serialize(amf_objects) 146 | 147 | rtmp_message = %RawMessage{message_type_id: 18, payload: binary} 148 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 149 | end 150 | 151 | test "Can unpack Video data message" do 152 | binary = <<1,2,3,4,5,6>> 153 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.VideoData{data: binary}} 154 | 155 | rtmp_message = %RawMessage{message_type_id: 9, payload: binary} 156 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 157 | end 158 | 159 | test "Can unpack Audio data message" do 160 | binary = <<1,2,3,4,5,6>> 161 | expected = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.AudioData{data: binary}} 162 | 163 | rtmp_message = %RawMessage{message_type_id: 8, payload: binary} 164 | assert {:ok, ^expected} = RawMessage.unpack(rtmp_message) 165 | end 166 | 167 | ## Detailed -> Raw tests (packing) 168 | 169 | test "Can convert SetChunkSize message to RawMessage" do 170 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.SetChunkSize{size: 4000}} 171 | rtmp_message = RawMessage.pack(message) 172 | 173 | assert %RawMessage{message_type_id: 1, payload: <<4000::32>>} = rtmp_message 174 | end 175 | 176 | test "Can convert Abort message to RawMessage" do 177 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Abort{stream_id: 500}} 178 | rtmp_message = RawMessage.pack(message) 179 | 180 | assert %RawMessage{message_type_id: 2, payload: <<500::32>>} = rtmp_message 181 | end 182 | 183 | test "Can convert Acknowledgement message to RawMessage" do 184 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Acknowledgement{sequence_number: 25}} 185 | rtmp_message = RawMessage.pack(message) 186 | 187 | assert %RawMessage{message_type_id: 3, payload: <<25::32>>} = rtmp_message 188 | end 189 | 190 | test "Can convert Window Acknowlegement Size message to RawMessage" do 191 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.WindowAcknowledgementSize{size: 26}} 192 | rtmp_message = RawMessage.pack(message) 193 | 194 | assert %RawMessage{message_type_id: 5, payload: <<26::32>>} = rtmp_message 195 | end 196 | 197 | test "Can convert Set Peer Bandwidth message to RawMessage" do 198 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.SetPeerBandwidth{window_size: 20, limit_type: :soft}} 199 | rtmp_message = RawMessage.pack(message) 200 | 201 | assert %RawMessage{message_type_id: 6, payload: <<20::32, 1::8>>} = rtmp_message 202 | end 203 | 204 | test "Can convert User Control Stream Begin to RawMessage" do 205 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 206 | type: :stream_begin, 207 | stream_id: 521, 208 | buffer_length: nil, 209 | timestamp: nil 210 | }} 211 | 212 | expected = %RawMessage{message_type_id: 4, payload: <<0::16, 521::32>>} 213 | assert ^expected = RawMessage.pack(message) 214 | end 215 | 216 | test "Can convert User Control Stream EOF to RawMessage" do 217 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 218 | type: :stream_eof, 219 | stream_id: 555, 220 | buffer_length: nil, 221 | timestamp: nil 222 | }} 223 | 224 | expected = %RawMessage{message_type_id: 4, payload: <<1::16, 555::32>>} 225 | assert ^expected = RawMessage.pack(message) 226 | end 227 | 228 | test "Can convert User Control Stream Dry to RawMessage" do 229 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 230 | type: :stream_dry, 231 | stream_id: 666, 232 | buffer_length: nil, 233 | timestamp: nil 234 | }} 235 | 236 | expected = %RawMessage{message_type_id: 4, payload: <<2::16, 666::32>>} 237 | assert ^expected = RawMessage.pack(message) 238 | end 239 | 240 | test "Can convert User Control Set Buffer Length to RawMessage" do 241 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 242 | type: :set_buffer_length, 243 | stream_id: 500, 244 | buffer_length: 300, 245 | timestamp: nil 246 | }} 247 | 248 | expected = %RawMessage{message_type_id: 4, payload: <<3::16, 500::32, 300::32>>} 249 | assert ^expected = RawMessage.pack(message) 250 | end 251 | 252 | test "Can convert User Control Stream Is Recorded to RawMessage" do 253 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 254 | type: :stream_is_recorded, 255 | stream_id: 333, 256 | buffer_length: nil, 257 | timestamp: nil 258 | }} 259 | 260 | expected = %RawMessage{message_type_id: 4, payload: <<4::16, 333::32>>} 261 | assert ^expected = RawMessage.pack(message) 262 | end 263 | 264 | test "Can convert User Control Ping Request to RawMessage" do 265 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 266 | type: :ping_request, 267 | stream_id: nil, 268 | buffer_length: nil, 269 | timestamp: 999 270 | }} 271 | 272 | expected = %RawMessage{message_type_id: 4, payload: <<6::16, 999::32>>} 273 | assert ^expected = RawMessage.pack(message) 274 | end 275 | 276 | test "Can convert User Control Ping Response to RawMessage" do 277 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.UserControl{ 278 | type: :ping_response, 279 | stream_id: nil, 280 | buffer_length: nil, 281 | timestamp: 900 282 | }} 283 | 284 | expected = %RawMessage{message_type_id: 4, payload: <<7::16, 900::32>>} 285 | assert ^expected = RawMessage.pack(message) 286 | end 287 | 288 | test "Can convert amf 0 encoded command message to RawMessage" do 289 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Amf0Command{ 290 | command_name: "something", 291 | transaction_id: 1221.0, 292 | command_object: nil, 293 | additional_values: ["test"] 294 | }} 295 | 296 | amf_objects = ["something", 1221.0, nil, "test"] 297 | binary = Amf0.serialize(amf_objects) 298 | 299 | expected = %RawMessage{message_type_id: 20, payload: binary} 300 | assert ^expected = RawMessage.pack(message) 301 | end 302 | 303 | test "Can convert amf0 encoded data message to RawMessage" do 304 | amf_objects = ["something", 1221.0, nil, "test"] 305 | 306 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.Amf0Data{parameters: amf_objects}} 307 | binary = Amf0.serialize(amf_objects) 308 | 309 | expected = %RawMessage{message_type_id: 18, payload: binary} 310 | assert ^expected = RawMessage.pack(message) 311 | end 312 | 313 | test "Can convert Video data message to RawMessage" do 314 | binary = <<1,2,3,4,5,6>> 315 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.VideoData{data: binary}} 316 | 317 | expected = %RawMessage{message_type_id: 9, payload: binary} 318 | assert ^expected = RawMessage.pack(message) 319 | end 320 | 321 | test "Can convert Audio data message to RawMessage" do 322 | binary = <<1,2,3,4,5,6>> 323 | message = %RtmpDetailedMessage{content: %Rtmp.Protocol.Messages.AudioData{data: binary}} 324 | 325 | expected = %RawMessage{message_type_id: 8, payload: binary} 326 | assert ^expected = RawMessage.pack(message) 327 | end 328 | 329 | test "Packing message transfers force uncompression flag when true" do 330 | message = %RtmpDetailedMessage{ 331 | content: %Rtmp.Protocol.Messages.SetChunkSize{size: 4000}, 332 | force_uncompressed: true 333 | } 334 | 335 | rtmp_message = RawMessage.pack(message) 336 | assert %RawMessage{force_uncompressed: true} = rtmp_message 337 | end 338 | 339 | test "Packing message transfers force uncompression flag when false" do 340 | message = %RtmpDetailedMessage{ 341 | content: %Rtmp.Protocol.Messages.SetChunkSize{size: 4000}, 342 | force_uncompressed: false 343 | } 344 | 345 | rtmp_message = RawMessage.pack(message) 346 | assert %RawMessage{force_uncompressed: false} = rtmp_message 347 | end 348 | end -------------------------------------------------------------------------------- /apps/rtmp/test/protocol/rtmp_time_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rtmp.Protocol.RtmpTimeTest do 2 | use ExUnit.Case, async: true 3 | doctest Rtmp.Protocol.RtmpTime 4 | end -------------------------------------------------------------------------------- /apps/rtmp/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(capture_log: true) 2 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/README.md: -------------------------------------------------------------------------------- 1 | # RtmpReaderCli 2 | 3 | Utility for parsing raw RTMP chunk binary file. It will go through the binary one RTMP message at a time and provide output useful for debugging an RTMP workflow. 4 | 5 | Each input file should only contain one direction of the stream (input or output), should not contain any bytes from the handshake, and should start from the first byte after the handshake (otherwise non-type 0 RTMP chunks may not resolve properly). 6 | 7 | This was made for use in debugging raw I/O dumps created by the `rtmp_session` module. -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :rtmp_reader_cli, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:rtmp_reader_cli, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/lib/rtmp_reader_cli.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpReaderCli do 2 | 3 | alias Rtmp.Protocol.ChunkIo, as: ChunkIo 4 | alias Rtmp.Protocol.RawMessage, as: RawMessage 5 | alias Rtmp.Protocol.DetailedMessage, as: DetailedMessage 6 | 7 | require Logger 8 | 9 | defmodule DisplayOptions do 10 | defstruct av_bytes_shown: 100 11 | end 12 | 13 | def main(args) do 14 | {options, _, _} = OptionParser.parse(args) 15 | 16 | file_path = Keyword.get(options, :file, :none) 17 | av_bytes_shown = Keyword.get(options, :av_bytes_shown, 100) 18 | binary = get_file_binary!(file_path) 19 | chunk_io = ChunkIo.new() 20 | 21 | IO.puts("Reading file '#{file_path}' (totalling #{byte_size(binary)} bytes)") 22 | IO.puts("RTMP messages will be displayed one at a time, enter will need to be called to proceed after each one") 23 | IO.puts("") 24 | 25 | display_options = %DisplayOptions{av_bytes_shown: av_bytes_shown} 26 | 27 | read_next_message(chunk_io, binary, 0, display_options) 28 | end 29 | 30 | defp get_file_binary!(file_path) do 31 | if file_path == :none do 32 | raise("no file specified") 33 | end 34 | 35 | case File.read(file_path) do 36 | {:error, reason} -> raise("Failed to open file: #{reason}") 37 | {:ok, binary} -> binary 38 | end 39 | 40 | end 41 | 42 | defp read_next_message(chunk_io, unparsed_binary, count_so_far, display_options) do 43 | {chunk_io, chunk_result} = ChunkIo.deserialize(chunk_io, unparsed_binary) 44 | case chunk_result do 45 | :incomplete -> 46 | IO.puts("No more data available") 47 | IO.puts("") 48 | 49 | :split_message -> 50 | read_next_message(chunk_io, <<>>, count_so_far, display_options) 51 | 52 | raw_message = %RawMessage{} -> 53 | IO.puts("Message ##{count_so_far}") 54 | chunk_io = case RawMessage.unpack(raw_message) do 55 | {:error, :unknown_message_type} -> 56 | IO.puts("Found message of type #{raw_message.message_type_id} but we have no known way to unpack it!") 57 | chunk_io 58 | 59 | {:ok, message = %DetailedMessage{content: %Rtmp.Protocol.Messages.SetChunkSize{}}} -> 60 | display_message_details(message, display_options) 61 | ChunkIo.set_receiving_max_chunk_size(chunk_io, message.content.size) 62 | 63 | {:ok, message} -> 64 | display_message_details(message, display_options) 65 | chunk_io 66 | end 67 | 68 | IO.puts("") 69 | _ = IO.gets("Press enter for next message.") 70 | read_next_message(chunk_io, <<>>, count_so_far + 1, display_options) 71 | end 72 | end 73 | 74 | defp display_message_details(message = %DetailedMessage{content: %Rtmp.Protocol.Messages.AudioData{}}, display_options) do 75 | IO.puts("Found message of type '#{message.content.__struct__}'") 76 | IO.puts("Timestamp: #{message.timestamp}") 77 | IO.puts("Stream Id: #{message.stream_id}") 78 | 79 | byte_count_to_show = display_options.av_bytes_shown 80 | {shown_bytes, suffix} = case message.content.data do 81 | <> -> {bytes, "..."} 82 | bytes -> {bytes, ""} 83 | end 84 | 85 | IO.puts("Content: #{Base.encode16(shown_bytes)}#{suffix}") 86 | end 87 | 88 | defp display_message_details(message = %DetailedMessage{content: %Rtmp.Protocol.Messages.VideoData{}}, display_options) do 89 | IO.puts("Found message of type '#{message.content.__struct__}'") 90 | IO.puts("Timestamp: #{message.timestamp}") 91 | IO.puts("Stream Id: #{message.stream_id}") 92 | 93 | byte_count_to_show = display_options.av_bytes_shown 94 | {shown_bytes, suffix} = case message.content.data do 95 | <> -> {bytes, "..."} 96 | bytes -> {bytes, ""} 97 | end 98 | 99 | IO.puts("Content: #{Base.encode16(shown_bytes)}#{suffix}") 100 | end 101 | 102 | defp display_message_details(message = %DetailedMessage{}, _display_options) do 103 | IO.puts("Found message of type '#{message.content.__struct__}'") 104 | IO.puts("Timestamp: #{message.timestamp}") 105 | IO.puts("Stream Id: #{message.stream_id}") 106 | IO.puts("Content: #{inspect(message.content)}") 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RtmpReaderCli.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :rtmp_reader_cli, 6 | version: "0.1.0", 7 | build_path: "../../_build", 8 | config_path: "../../config/config.exs", 9 | deps_path: "../../deps", 10 | lockfile: "../../mix.lock", 11 | elixir: "~> 1.3", 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps(), 15 | escript: [ 16 | main_module: RtmpReaderCli, 17 | name: "rtmp_reader_cli.escript" 18 | ]] 19 | end 20 | 21 | def application do 22 | [applications: [:logger]] 23 | end 24 | 25 | defp deps do 26 | [{:rtmp, in_umbrella: true}] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/test/rtmp_reader_cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RtmpReaderCliTest do 2 | use ExUnit.Case 3 | doctest RtmpReaderCli 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/rtmp_reader_cli/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/README.md: -------------------------------------------------------------------------------- 1 | # SimpleRtmpPlayer 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `simple_rtmp_player` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [{:simple_rtmp_player, "~> 0.1.0"}] 13 | end 14 | ``` 15 | 16 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 17 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 18 | be found at [https://hexdocs.pm/simple_rtmp_player](https://hexdocs.pm/simple_rtmp_player). 19 | 20 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :simple_rtmp_player, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:simple_rtmp_player, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/lib/simple_rtmp_player.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpPlayer do 2 | 3 | def main(args) do 4 | {host, port, app, key} = parse_args(args) 5 | connection_info = %GenRtmpClient.ConnectionInfo{ 6 | host: host, 7 | port: String.to_integer(port), 8 | app_name: app, 9 | connection_id: "test-player" 10 | } 11 | 12 | IO.puts("Host: #{connection_info.host}") 13 | IO.puts("Port: #{connection_info.port}") 14 | IO.puts("App: #{connection_info.app_name}") 15 | IO.puts("Stream Key: #{key}") 16 | 17 | {:ok, _client_pid} = GenRtmpClient.start_link(SimpleRtmpPlayer.Client, connection_info, key) 18 | loop() 19 | end 20 | 21 | defp parse_args([host, port, app, key]), do: {host, port, app, key} 22 | defp parse_args(_), do: raise("Invalid parameters passed in") 23 | 24 | defp loop() do 25 | receive do 26 | _arg -> :ok 27 | end 28 | 29 | loop() 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/lib/simple_rtmp_player/client.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpPlayer.Client do 2 | @behaviour GenRtmpClient 3 | 4 | alias Rtmp.ClientSession.Events, as: Events 5 | 6 | require Logger 7 | 8 | defmodule State do 9 | defstruct status: :started, 10 | connection_info: nil, 11 | stream_key: nil, 12 | av_bytes_received: 0, 13 | last_av_announcement: 0 14 | end 15 | 16 | def init(connection_info, stream_key) do 17 | _ = Logger.debug("Initialized: #{inspect(connection_info)} with stream key: #{stream_key}") 18 | state = %State{connection_info: connection_info, stream_key: stream_key} 19 | {:ok, state} 20 | end 21 | 22 | def handle_connection_response(%Events.ConnectionResponseReceived{was_accepted: true}, state) do 23 | _ = Logger.debug("Connection accepted, requesting playback on stream key") 24 | 25 | GenRtmpClient.start_playback(self(), state.stream_key) 26 | 27 | state = %{state | status: :connected} 28 | {:ok, state} 29 | end 30 | 31 | def handle_play_response(response, state) do 32 | _ = Logger.debug("Play response received: #{inspect(response)}") 33 | {:ok, state} 34 | end 35 | 36 | def handle_play_reset(event = %Events.PlayResetReceived{}, state) do 37 | _ = Logger.debug("Play reset received: #{event.description}") 38 | {:ok, state} 39 | end 40 | 41 | def handle_publish_response(response, state) do 42 | _ = Logger.debug("Publish response received: #{inspect(response)}") 43 | {:ok, state} 44 | end 45 | 46 | def handle_metadata_received(metadata, state) do 47 | _ = Logger.debug("Metadata received: #{inspect(metadata)}") 48 | {:ok, state} 49 | end 50 | 51 | def handle_av_data_received(av_message, state) do 52 | state = %{state | av_bytes_received: state.av_bytes_received + byte_size(av_message.data)} 53 | state = cond do 54 | state.av_bytes_received - state.last_av_announcement > 10_1000 -> 55 | _ = Logger.debug("Received #{state.av_bytes_received} bytes of a/v data") 56 | %{state | last_av_announcement: state.av_bytes_received} 57 | 58 | true -> state 59 | end 60 | 61 | {:ok, state} 62 | end 63 | 64 | def handle_disconnection(reason, state) do 65 | _ = Logger.debug("Disconnected: #{inspect(reason)}") 66 | {:stop, state} 67 | end 68 | 69 | def byte_io_totals_updated(event, state) do 70 | _ = Logger.debug("IO totals updated: #{inspect(event)}") 71 | {:ok, state} 72 | end 73 | 74 | def handle_message(message, state) do 75 | _ = Logger.warn("#{state.connection_info.connection_id}: Unable to handle client message: #{message}") 76 | {:ok, state} 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /apps/simple_rtmp_player/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpPlayer.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :simple_rtmp_player, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps(), 16 | escript: [ 17 | main_module: SimpleRtmpPlayer, 18 | name: "simple_rtmp_player.escript" 19 | ] 20 | ] 21 | end 22 | 23 | def application do 24 | [extra_applications: [:logger]] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:gen_rtmp_client, in_umbrella: true} 30 | ] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/test/simple_rtmp_player_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpPlayerTest do 2 | use ExUnit.Case 3 | doctest SimpleRtmpPlayer 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/simple_rtmp_player/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/README.md: -------------------------------------------------------------------------------- 1 | # SimpleRtmpProxy 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `simple_rtmp_proxy` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [{:simple_rtmp_proxy, "~> 0.1.0"}] 13 | end 14 | ``` 15 | 16 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 17 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 18 | be found at [https://hexdocs.pm/simple_rtmp_proxy](https://hexdocs.pm/simple_rtmp_proxy). 19 | 20 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :simple_rtmp_proxy, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:simple_rtmp_proxy, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/lib/simple_rtmp_proxy.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpProxy do 2 | 3 | def main(args) do 4 | import Supervisor.Spec, warn: false 5 | 6 | {in_port, host, out_port, app} = parse_args(args) 7 | in_port = String.to_integer(in_port) 8 | out_port = String.to_integer(out_port) 9 | 10 | children = [worker(SimpleRtmpProxy.ServerWorker, [in_port, host, out_port, app])] 11 | opts = [strategy: :one_for_one, name: SimpleRtmpProxy.Supervisor] 12 | Supervisor.start_link(children, opts) 13 | 14 | Process.sleep(:infinity) 15 | end 16 | 17 | defp parse_args([in_port, host, out_port, app]), do: {in_port, host, out_port, app} 18 | defp parse_args(_), do: raise("Expected parameters: input_port output_host output_port output_app") 19 | end 20 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/lib/simple_rtmp_proxy/client.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpProxy.Client do 2 | @behaviour GenRtmpClient 3 | 4 | alias Rtmp.ClientSession.Events, as: Events 5 | require Logger 6 | 7 | defmodule State do 8 | @moduledoc false 9 | 10 | defstruct status: :started, 11 | connection_info: nil, 12 | stream_key: nil, 13 | video_header: nil, 14 | audio_header: nil, 15 | metadata: nil, 16 | has_sent_keyframe: false 17 | end 18 | 19 | @spec relay_av_data(pid, Rtmp.ClientSession.Handler.av_type, Rtmp.timestamp, binary) :: :ok 20 | def relay_av_data(pid, type, timestamp, data) do 21 | _ = send(pid, {:relay_av, type, timestamp, data}) 22 | :ok 23 | end 24 | 25 | def init(connection_info, [stream_key, video_header, audio_header, metadata]) do 26 | _ = Logger.debug("#{connection_info.connection_id}: client initialized for stream key #{stream_key}") 27 | state = %State{ 28 | connection_info: connection_info, 29 | stream_key: stream_key, 30 | video_header: video_header, 31 | audio_header: audio_header, 32 | metadata: metadata 33 | } 34 | 35 | {:ok, state} 36 | end 37 | 38 | def handle_connection_response(%Events.ConnectionResponseReceived{was_accepted: true}, state) do 39 | _ = Logger.debug("#{state.connection_info.connection_id}: Connection accepted, requesting publishing") 40 | 41 | GenRtmpClient.start_publish(self(), state.stream_key, :live) 42 | 43 | state = %{state | status: :connected} 44 | {:ok, state} 45 | end 46 | 47 | def handle_play_response(_respponse, state) do 48 | raise("#{state.connection_info.connection_id}: Received playback response but playback should have never been requested") 49 | end 50 | 51 | def handle_play_reset(_event, state) do 52 | raise("#{state.connection_info.connection_id}: Received play reset command but playback isn't expected'") 53 | end 54 | 55 | def handle_metadata_received(_metadata, state) do 56 | _ = Logger.warn("#{state.connection_info.connection_id}: Metadata received unexpectedly") 57 | {:ok, state} 58 | end 59 | 60 | def handle_av_data_received(_av_message, state) do 61 | _ = Logger.warn("#{state.connection_info.connection_id}: AV data received unexpectedly") 62 | 63 | {:ok, state} 64 | end 65 | 66 | def handle_publish_response(event = %Events.PublishResponseReceived{was_accepted: true}, state) do 67 | _ = Logger.debug("#{state.connection_info.connection_id}: Publish accepted for stream key #{event.stream_key}") 68 | 69 | :ok = GenRtmpClient.publish_metadata(self(), state.stream_key, state.metadata) 70 | :ok = GenRtmpClient.publish_av_data(self(), state.stream_key, :audio, 0, state.audio_header) 71 | :ok = GenRtmpClient.publish_av_data(self(), state.stream_key, :video, 0, state.video_header) 72 | 73 | state = %{state | status: :publishing} 74 | {:ok, state} 75 | end 76 | 77 | def handle_disconnection(reason, state) do 78 | _ = Logger.debug("#{state.connection_info.connection_id}: Disconnected - #{inspect(reason)}") 79 | 80 | case reason do 81 | :stopping -> {:stop, state} 82 | _ -> {:retry, state} 83 | end 84 | end 85 | 86 | def byte_io_totals_updated(_event, state) do 87 | {:ok, state} 88 | end 89 | 90 | def handle_message({:relay_av, type, timestamp, data}, state = %State{status: :publishing}) do 91 | should_relay_data = case {state.has_sent_keyframe, type, is_keyframe(data)} do 92 | {true, _, _} -> true 93 | {false, :video, true} -> true 94 | {false, _, _} -> false 95 | end 96 | 97 | state = case should_relay_data do 98 | false -> state 99 | 100 | true -> 101 | :ok = GenRtmpClient.publish_av_data(self(), state.stream_key, type, timestamp, data) 102 | %{state | has_sent_keyframe: true} 103 | end 104 | 105 | {:ok, state} 106 | end 107 | 108 | def handle_message({:relay_av, _type, _timestamp, _data}, state) do 109 | # ignore since we aren't publishing 110 | {:ok, state} 111 | end 112 | 113 | def handle_message(message, state) do 114 | _ = Logger.debug("#{state.connection_info.connection_id}: Unknown message received: #{inspect(message)}") 115 | 116 | {:ok, state} 117 | end 118 | 119 | defp is_keyframe(<<0x17, _::binary>>), do: true 120 | defp is_keyframe(_), do: false 121 | end -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/lib/simple_rtmp_proxy/server_worker.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpProxy.ServerWorker do 2 | @moduledoc """ 3 | Simple implementation of a RTMP server that relays video to another 4 | RTMP server. 5 | 6 | RTMP playback clients will be disconnected 7 | """ 8 | 9 | alias Rtmp.ServerSession.Events, as: RtmpEvents 10 | require Logger 11 | 12 | @behaviour GenRtmpServer 13 | 14 | defmodule State do 15 | @moduledoc false 16 | 17 | defstruct session_id: nil, 18 | client_ip: nil, 19 | stream_key: nil, 20 | metadata: nil, 21 | video_sequence_header: nil, 22 | audio_sequence_header: nil, 23 | client_info: nil, 24 | client_pid: nil 25 | end 26 | 27 | defmodule ClientInfo do 28 | @moduledoc false 29 | 30 | defstruct host: nil, 31 | port: nil, 32 | app_name: nil 33 | end 34 | 35 | def start_link(in_port, host, out_port, app) do 36 | options = %GenRtmpServer.RtmpOptions{port: in_port} 37 | GenRtmpServer.start_link(__MODULE__, options, [host, out_port, app]) 38 | end 39 | 40 | def init(session_id, client_ip, [host, port, app_name]) do 41 | _ = Logger.info "#{session_id}: simple rtmp proxy session started" 42 | 43 | state = %State{ 44 | session_id: session_id, 45 | client_ip: client_ip, 46 | client_info: %ClientInfo{ 47 | host: host, 48 | port: port, 49 | app_name: app_name 50 | } 51 | } 52 | 53 | {:ok, state} 54 | end 55 | 56 | def connection_requested(%RtmpEvents.ConnectionRequested{}, state = %State{}) do 57 | {:accepted, state} 58 | end 59 | 60 | def publish_requested(event = %RtmpEvents.PublishStreamRequested{}, state = %State{}) do 61 | case state.stream_key do 62 | nil -> 63 | state = %{state | stream_key: event.stream_key} 64 | {:accepted, state} 65 | 66 | _ -> 67 | {{:rejected, :ignore, "Publish already in progress on stream key #{state.stream_key}"}, state} 68 | end 69 | end 70 | 71 | def publish_finished(%RtmpEvents.PublishingFinished{}, state = %State{}) do 72 | {:ok, state} 73 | end 74 | 75 | def play_requested(%RtmpEvents.PlayStreamRequested{}, state = %State{}) do 76 | {{:rejected, :disconnect, "No playback allowed"}, state} 77 | end 78 | 79 | def play_finished(%RtmpEvents.PlayStreamFinished{}, state = %State{}) do 80 | {:ok, state} 81 | end 82 | 83 | def metadata_received(event = %RtmpEvents.StreamMetaDataChanged{}, state = %State{}) do 84 | if event.stream_key != state.stream_key do 85 | message = "#{state.session_id}: Received stream metadata on stream key #{event.stream_key} but " <> 86 | "currently publishing on stream key #{state.stream_key}" 87 | raise(message) 88 | end 89 | 90 | state = %{state | metadata: event.meta_data } 91 | {:ok, state} 92 | end 93 | 94 | def audio_video_data_received(event = %RtmpEvents.AudioVideoDataReceived{}, state = %State{}) do 95 | if event.stream_key != state.stream_key do 96 | message = "#{state.session_id}: Received a/v data on stream key #{event.stream_key} but " <> 97 | "currently publishing on stream key #{state.stream_key}" 98 | raise(message) 99 | end 100 | 101 | state = case event.data_type do 102 | :audio -> 103 | case is_audio_sequence_header(event.data) do 104 | false -> state 105 | true -> 106 | _ = Logger.debug("#{state.session_id}: Audio sequence header received") 107 | %{state | audio_sequence_header: event.data} 108 | end 109 | 110 | :video -> 111 | case is_video_sequence_header(event.data) do 112 | false -> state 113 | true -> 114 | _ = Logger.debug("#{state.session_id}: Video sequence header received") 115 | %{state | video_sequence_header: event.data } 116 | end 117 | end 118 | 119 | state = start_client_if_ready(state) 120 | if state.client_pid != nil do 121 | :ok = SimpleRtmpProxy.Client.relay_av_data(state.client_pid, event.data_type, event.timestamp, event.data) 122 | end 123 | 124 | {:ok, state} 125 | end 126 | 127 | def byte_io_totals_updated(%RtmpEvents.NewByteIOTotals{}, state = %State{}) do 128 | {:ok, state} 129 | end 130 | 131 | def acknowledgement_received(%RtmpEvents.AcknowledgementReceived{}, state = %State{}) do 132 | {:ok, state} 133 | end 134 | 135 | def ping_request_sent(%RtmpEvents.PingRequestSent{}, state = %State{}) do 136 | {:ok, state} 137 | end 138 | 139 | def ping_response_received(%RtmpEvents.PingResponseReceived{}, state = %State{}) do 140 | {:ok, state} 141 | end 142 | 143 | def handle_disconnection(state = %State{}) do 144 | if state.client_pid != nil do 145 | _ = Logger.debug("#{state.session_id}: Stopping client due to disconnection") 146 | :ok = GenRtmpClient.stop_client(state.client_pid) 147 | end 148 | 149 | {:ok, state} 150 | end 151 | 152 | def code_change(_, state = %State{}) do 153 | {:ok, state} 154 | end 155 | 156 | def handle_message(message, state = %State{}) do 157 | _ = Logger.debug("#{state.session_id}: Unknown message received: #{inspect(message)}") 158 | {:ok, state} 159 | end 160 | 161 | defp start_client_if_ready(state) do 162 | cond do 163 | state.client_pid != nil -> state 164 | state.video_sequence_header == nil -> state 165 | state.audio_sequence_header == nil -> state 166 | state.metadata == nil -> state 167 | 168 | true -> 169 | connection_info = %GenRtmpClient.ConnectionInfo{ 170 | host: state.client_info.host, 171 | port: state.client_info.port, 172 | app_name: state.client_info.app_name, 173 | connection_id: state.session_id <> "_client" 174 | } 175 | 176 | client_args = [ 177 | state.stream_key, 178 | state.video_sequence_header, 179 | state.audio_sequence_header, 180 | state.metadata 181 | ] 182 | 183 | {:ok, client_pid} = GenRtmpClient.start_link(SimpleRtmpProxy.Client, connection_info, client_args) 184 | 185 | %{state | client_pid: client_pid} 186 | end 187 | end 188 | 189 | defp is_video_sequence_header(<<0x17, 0x00, _::binary>>), do: true 190 | defp is_video_sequence_header(_), do: false 191 | 192 | defp is_audio_sequence_header(<<0xaf, 0x00, _::binary>>), do: true 193 | defp is_audio_sequence_header(_), do: false 194 | 195 | end -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpProxy.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :simple_rtmp_proxy, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.4", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps(), 16 | escript: [ 17 | main_module: SimpleRtmpProxy, 18 | name: "simple_rtmp_proxy.escript" 19 | ], 20 | ] 21 | end 22 | 23 | def application do 24 | [extra_applications: [:logger]] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:gen_rtmp_server, in_umbrella: true}, 30 | {:gen_rtmp_client, in_umbrella: true}, 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/test/simple_rtmp_proxy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpProxyTest do 2 | use ExUnit.Case 3 | doctest SimpleRtmpProxy 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/simple_rtmp_proxy/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/README.md: -------------------------------------------------------------------------------- 1 | # SimpleRtmpServer 2 | 3 | The simple RTMP server is a very basic example of a `GenRtmpServer` implementation. It is meant to test the publication and playback of a server with very little logic surrounding it. It therefore accepts all RTMP requests and takes very little actions on top of it besides routing audio, video, and metadata values between connected publishers and players. 4 | 5 | Starting a server on port 1935 is as simple as running: 6 | 7 | `mix run --no-halt` -------------------------------------------------------------------------------- /apps/simple_rtmp_server/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :simple_rtmp_server, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:simple_rtmp_server, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/lib/simple_rtmp_server.ex: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpServer do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | worker(SimpleRtmpServer.Worker, []) 12 | ] 13 | 14 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 15 | # for other strategies and supported options 16 | opts = [strategy: :one_for_one, name: SimpleRtmpServer.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpServer.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :simple_rtmp_server, 6 | version: "0.1.0", 7 | build_path: "../../_build", 8 | config_path: "../../config/config.exs", 9 | deps_path: "../../deps", 10 | lockfile: "../../mix.lock", 11 | elixir: "~> 1.3", 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps()] 15 | end 16 | 17 | def application do 18 | [applications: [:logger], 19 | mod: {SimpleRtmpServer, []}] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:gen_rtmp_server, in_umbrella: true}, 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/test/simple_rtmp_server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleRtmpServerTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/simple_rtmp_server/ttb_last_config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KallDrexx/elixir-media-libs/5115c390133696a01c1b24107fa1f51eca9ce7b4/apps/simple_rtmp_server/ttb_last_config -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, ensuring 7 | # they all use the same configuration. While one could 8 | # configure all applications here, we prefer to delegate 9 | # back to each application for organization purposes. 10 | import_config "../apps/*/config/config.exs" 11 | 12 | # Sample configuration (overrides the imported configuration above): 13 | # 14 | # config :logger, :console, 15 | # level: :info, 16 | # format: "$date $time [$level] $metadata$message\n", 17 | # metadata: [:user_id] 18 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirMediaLibs.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [apps_path: "apps", 6 | build_embedded: Mix.env == :prod, 7 | start_permanent: Mix.env == :prod, 8 | deps: deps(), 9 | dialyzer: [plt_add_deps: :transitive] 10 | ] 11 | end 12 | 13 | defp deps do 14 | [{:dialyxir, "~> 0.4.3", only: [:dev, :umbrella]}] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"amf0": {:hex, :eml_amf0, "1.0.1", "b14646bf5697a67a269e42aa6a331f8b364b040af0782d7281c412fd15978fa9", [:mix], []}, 2 | "dialyxir": {:hex, :dialyxir, "0.4.3", "a4daeebd0107de10d3bbae2ccb6b8905e69544db1ed5fe9148ad27cd4cb2c0cd", [:mix], []}, 3 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 5 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}, 6 | "rtmp": {:hex, :eml_rtmp, "0.2.0", "2e6ec416994b7b4f92920c8507ed72c10307e5594bc92e82e53e00f52d108d61", [:mix], [{:amf0, "~> 1.0.1", [hex: :eml_amf0, optional: false]}]}, 7 | "rtmp_handshake": {:hex, :eml_rtmp_handshake, "1.0.0", "cb1954297c224639f4bcbb55633a87ca7b22ade2a51a3625504f0bb6da72c59c", [:mix], []}, 8 | "rtmp_session": {:hex, :eml_rtmp_session, "0.1.0", "6b24ec82d868102a3054e1488edeca4b6a56b84f98dd5fadaad0c193101c4a0b", [:mix], [{:amf0, "~> 1.0", [hex: :eml_amf0, optional: false]}]}, 9 | "uuid": {:hex, :uuid, "1.1.3", "06ca38801a1a95b751701ca40716bb97ddf76dfe7e26da0eec7dba636740d57a", [:mix], []}} 10 | --------------------------------------------------------------------------------