Current section
Files
Jump to
Current section
Files
src/media_broker_srv.erl
-module(media_broker_srv).
-behaviour(gen_server).
-compile(nowarn_deprecated_catch).
-include_lib("n2o/include/n2o.hrl").
%% API
-export([start_link/0, peer_joined/4, sdp_answer/4, ice_candidate/4, peer_left/3, terminate_room/2, recording_path/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-record(state, {
ports = #{}, % RoomId :: binary() -> Port :: port()
room_peers = #{}, % RoomId :: binary() -> [PeerId :: binary()]
peer_rooms = #{}, % PeerId :: binary() -> RoomId :: binary()
room_started_at = #{}, % RoomId :: binary() -> Timestamp :: integer()
monitors = #{} % MonitorRef :: reference() -> {RoomId, PeerId}
}).
%% API Functions
start_link() ->
gen_server:start_link(?MODULE, [], []).
peer_joined(BrokerPid, RoomId, PeerId, ClientPid) ->
gen_server:call(BrokerPid, {peer_joined, RoomId, PeerId, ClientPid}).
sdp_answer(BrokerPid, RoomId, PeerId, Sdp) ->
gen_server:cast(BrokerPid, {sdp_answer, RoomId, PeerId, Sdp}).
ice_candidate(BrokerPid, RoomId, PeerId, Candidate) ->
gen_server:cast(BrokerPid, {ice_candidate, RoomId, PeerId, Candidate}).
peer_left(BrokerPid, RoomId, PeerId) ->
gen_server:cast(BrokerPid, {peer_left, RoomId, PeerId}).
terminate_room(BrokerPid, RoomId) ->
gen_server:call(BrokerPid, {terminate_room, RoomId}).
recording_path(RoomId) ->
case RoomId of
<<"court-room-room123">> -> filename:absname("priv/static/rooms/court-room-room123/index.m3u8");
_ -> filename:absname("priv/static/rooms/" ++ binary_to_list(RoomId) ++ "/index.m3u8")
end.
%% gen_server Callbacks
init([]) ->
process_flag(trap_exit, true),
{ok, #state{}}.
handle_call({peer_joined, RoomId, PeerId, ClientPid}, _From, State) ->
{Port, NewPorts, StartedAt, NewStartedAt} = case maps:find(RoomId, State#state.ports) of
{ok, P} ->
{P, State#state.ports, maps:get(RoomId, State#state.room_started_at), State#state.room_started_at};
error ->
Now = erlang:system_time(millisecond),
Binary = filename:absname(find_binary()),
OutDir = case RoomId of
<<"court-room-room123">> -> filename:absname("priv/static/rooms/court-room-room123");
_ -> filename:absname("priv/static/rooms/" ++ binary_to_list(RoomId))
end,
filelib:ensure_dir(OutDir ++ "/"),
os:cmd("rm -f " ++ OutDir ++ "/*"),
error_logger:info_msg("Spawning GStreamer mixer for room ~s writing to ~s~n", [RoomId, OutDir]),
HlsFormat = application:get_env(rtp, hls_format, fmp4),
FormatStr = atom_to_list(HlsFormat),
Args = [OutDir, FormatStr],
error_logger:info_msg("Starting GStreamer MCU: ~p ~p~n", [Binary, Args]),
P = open_port({spawn_executable, Binary}, [
binary,
stream,
{args, Args},
use_stdio,
stderr_to_stdout,
exit_status,
{line, 16384},
{env, [
{"GST_GL_WINDOW", "none"},
{"GST_PLUGIN_FEATURE_FILTER", "opengl:0,applemedia:0"}
]}
]),
{P, maps:put(RoomId, P, State#state.ports), Now, maps:put(RoomId, Now, State#state.room_started_at)}
end,
send_to_port(Port, #{
<<"type">> => <<"peer_joined">>,
<<"peer_id">> => PeerId
}),
Ref = monitor(process, ClientPid),
NewMonitors = maps:put(Ref, {RoomId, PeerId}, State#state.monitors),
Peers = maps:get(RoomId, State#state.room_peers, []),
NewRoomPeers = maps:put(RoomId, [PeerId | Peers], State#state.room_peers),
NewPeerRooms = maps:put(PeerId, RoomId, State#state.peer_rooms),
PlaylistPath = recording_path(RoomId),
Status = case filelib:is_regular(PlaylistPath) of
true -> ok;
false -> pending
end,
{reply, {Status, StartedAt}, State#state{
ports = NewPorts,
room_peers = NewRoomPeers,
peer_rooms = NewPeerRooms,
room_started_at = NewStartedAt,
monitors = NewMonitors
}};
handle_call({terminate_room, RoomId}, _From, State) ->
case maps:find(RoomId, State#state.ports) of
{ok, Port} ->
error_logger:info_msg("Terminating GStreamer for room ~s, finalizing mp4~n", [RoomId]),
send_to_port(Port, #{<<"type">> => <<"exit">>}),
{reply, {ok, recording_path(RoomId)}, State};
error ->
{reply, {error, not_found}, State}
end;
handle_call(_Request, _From, State) ->
{reply, {error, unknown_call}, State}.
handle_cast({sdp_answer, RoomId, PeerId, Sdp}, State) ->
case maps:find(RoomId, State#state.ports) of
{ok, Port} ->
send_to_port(Port, #{
<<"type">> => <<"sdp_answer">>,
<<"peer_id">> => PeerId,
<<"sdp">> => Sdp
});
error ->
ok
end,
{noreply, State};
handle_cast({ice_candidate, RoomId, PeerId, Candidate}, State) ->
case maps:find(RoomId, State#state.ports) of
{ok, Port} ->
send_to_port(Port, #{
<<"type">> => <<"ice_candidate">>,
<<"peer_id">> => PeerId,
<<"candidate">> => Candidate
});
error ->
ok
end,
{noreply, State};
handle_cast({peer_left, RoomId, PeerId}, State) ->
State1 = handle_peer_departure(RoomId, PeerId, State),
{noreply, State1};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({Port, {data, {eol, LineBin}}}, State) ->
case catch jsone:decode(LineBin) of
#{<<"type">> := <<"sdp_offer">>, <<"peer_id">> := PeerId, <<"sdp">> := Sdp} ->
case syn:lookup(rooms, PeerId) of
{Pid, _} -> Pid ! {sdp_offer, Sdp};
undefined -> ok
end,
{noreply, State};
#{<<"type">> := <<"ice_candidate">>, <<"peer_id">> := PeerId, <<"candidate">> := Candidate} ->
case syn:lookup(rooms, PeerId) of
{Pid, _} -> Pid ! {ice_candidate, Candidate};
undefined -> ok
end,
{noreply, State};
#{<<"type">> := <<"recording_started">>} ->
RoomId = find_room_by_port(Port, State#state.ports),
case RoomId of
undefined -> {noreply, State};
_ ->
RealStart = erlang:system_time(millisecond),
self() ! {poll_manifest, RoomId, RealStart, 0},
NewStartedAt = maps:put(RoomId, RealStart, State#state.room_started_at),
{noreply, State#state{room_started_at = NewStartedAt}}
end;
_Other ->
error_logger:info_msg("GStreamer Output (~p): ~s~n", [self(), LineBin]),
{noreply, State}
end;
handle_info({Port, {exit_status, Status}}, State) ->
error_logger:info_msg("GStreamer port ~p exited with status ~p~n", [Port, Status]),
RoomId = find_room_by_port(Port, State#state.ports),
case RoomId of
undefined ->
{noreply, State};
_ ->
NewPorts = maps:remove(RoomId, State#state.ports),
Peers = maps:get(RoomId, State#state.room_peers, []),
NewPeerRooms = lists:foldl(fun(P, M) -> maps:remove(P, M) end, State#state.peer_rooms, Peers),
NewRoomPeers = maps:remove(RoomId, State#state.room_peers),
{noreply, State#state{
ports = NewPorts,
room_peers = NewRoomPeers,
peer_rooms = NewPeerRooms
}}
end;
handle_info({'EXIT', From, Reason}, State) ->
case is_port(From) of
true ->
{noreply, State};
false ->
error_logger:info_msg("media_broker_srv parent exited: ~p (~p). Terminating.~n", [From, Reason]),
{stop, Reason, State}
end;
handle_info({'DOWN', Ref, process, _Pid, _Reason}, State) ->
case maps:find(Ref, State#state.monitors) of
{ok, {RoomId, PeerId}} ->
NewMonitors = maps:remove(Ref, State#state.monitors),
Peers = maps:get(RoomId, State#state.room_peers, []),
case lists:member(PeerId, Peers) of
true ->
error_logger:info_msg("media_broker_srv: Peer ~s process died. Cleaning up.~n", [PeerId]),
State1 = handle_peer_departure(RoomId, PeerId, State),
{noreply, State1#state{monitors = NewMonitors}};
false ->
{noreply, State#state{monitors = NewMonitors}}
end;
error ->
{noreply, State}
end;
handle_info({poll_manifest, RoomId, StartedAt, Attempts}, State) ->
PlaylistPath = recording_path(RoomId),
case filelib:is_regular(PlaylistPath) of
true ->
error_logger:info_msg("HLS manifest ~s is ready on disk. Notifying clients.~n", [PlaylistPath]),
notify_room_info(RoomId, StartedAt, State),
{noreply, State};
false ->
if
Attempts < 1000 ->
erlang:send_after(100, self(), {poll_manifest, RoomId, StartedAt, Attempts + 1}),
{noreply, State};
true ->
error_logger:error_msg("Timeout waiting for HLS manifest ~s~n", [PlaylistPath]),
{noreply, State}
end
end;
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
maps:map(fun(_, Port) -> catch port_close(Port) end, State#state.ports),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal Helpers
send_to_port(Port, Map) ->
Json = jsone:encode(Map),
port_command(Port, [Json, <<"\n">>]).
notify_room_info(RoomId, StartedAt, State) ->
HlsFormat = application:get_env(rtp, hls_format, fmp4),
RoomInfoMsg = jsone:encode(#{
<<"type">> => <<"room_info">>,
<<"started_at">> => StartedAt,
<<"hls_format">> => HlsFormat
}),
Msg = {'$msg', kvs:seq([], []), [], [], <<"System">>, RoomInfoMsg},
n2o:send({topic, binary_to_list(RoomId)}, #client{data = Msg}),
Peers = maps:get(RoomId, State#state.room_peers, []),
lists:foreach(fun(PeerId) ->
case syn:lookup(rooms, PeerId) of
{Pid, _} -> Pid ! {send_room_info, StartedAt};
undefined -> ok
end
end, Peers).
handle_peer_departure(RoomId, PeerId, State) ->
case maps:find(RoomId, State#state.ports) of
{ok, Port} ->
send_to_port(Port, #{
<<"type">> => <<"peer_left">>,
<<"peer_id">> => PeerId
}),
Peers = maps:get(RoomId, State#state.room_peers, []),
NewPeers = lists:delete(PeerId, Peers),
NewPeerRooms = maps:remove(PeerId, State#state.peer_rooms),
case NewPeers of
[] ->
error_logger:info_msg("Last peer left room ~s. Closing GStreamer port.~n", [RoomId]),
catch port_close(Port),
NewPorts = maps:remove(RoomId, State#state.ports),
NewRoomPeers = maps:remove(RoomId, State#state.room_peers),
State#state{
ports = NewPorts,
room_peers = NewRoomPeers,
peer_rooms = NewPeerRooms
};
_ ->
NewRoomPeers = maps:put(RoomId, NewPeers, State#state.room_peers),
State#state{
room_peers = NewRoomPeers,
peer_rooms = NewPeerRooms
}
end;
error ->
State
end.
find_room_by_port(Port, Ports) ->
List = maps:to_list(Ports),
case lists:keyfind(Port, 2, List) of
{RoomId, _} -> RoomId;
false -> undefined
end.
find_binary() ->
Paths = [
case code:priv_dir(rtp) of
{error, bad_name} -> "./priv/gst";
D -> filename:join(D, "gst")
end,
"./priv/gst",
"../priv/gst",
"../../priv/gst"
],
find_existing_path(Paths).
find_existing_path([Path | T]) ->
case filelib:is_regular(Path) of
true -> Path;
false -> find_existing_path(T)
end;
find_existing_path([]) ->
"./priv/gst".