PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/whistle_apps/apps/media_mgr/src/media_shout.erl

http://github.com/2600hz/whistle
Erlang | 369 lines | 236 code | 43 blank | 90 comment | 4 complexity | 2de40b340981d1f7d9046ee4943d0852 MD5 | raw file
Possible License(s): GPL-3.0, MPL-2.0-no-copyleft-exception, Apache-2.0, JSON, BSD-3-Clause, Unlicense, LGPL-3.0, MIT
  1. %%%-------------------------------------------------------------------
  2. %%% @author James Aimonetti <james@2600hz.org>
  3. %%% @copyright (C) 2011, VoIP INC
  4. %%% @doc
  5. %%% Server to stream an MP3 or WAVE file, once or continuously
  6. %%% @end
  7. %%% Created : 15 Mar 2011 by James Aimonetti <james@2600hz.org>
  8. %%%-------------------------------------------------------------------
  9. -module(media_shout).
  10. -behaviour(gen_server).
  11. %% API
  12. -export([start_link/5, stop/1]).
  13. %% gen_server callbacks
  14. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
  15. terminate/2, code_change/3]).
  16. -include("media.hrl").
  17. -define(SERVER, ?MODULE).
  18. -define(CONTENT_TYPE_MP3, <<"audio/mpeg">>).
  19. -define(CONTENT_TYPE_WAV, <<"audio/x-wav">>).
  20. -define(MAX_FETCH_RETRIES, 5).
  21. -record(state, {
  22. media_file = #media_file{} :: #media_file{}
  23. ,media_id = <<>> :: binary()
  24. ,lsocket = undefined :: undefined | port()
  25. ,db = <<>> :: binary()
  26. ,doc = <<>> :: binary()
  27. ,attachment = <<>> :: binary()
  28. ,media_name = <<>> :: binary()
  29. ,content_type = undefined :: undefined | binary()
  30. ,send_to = [] :: list(binary()) | []
  31. ,stream_type = single :: single | continuous
  32. ,media_loop = undefined :: undefined | pid()
  33. }).
  34. %%%===================================================================
  35. %%% API
  36. %%%===================================================================
  37. %%--------------------------------------------------------------------
  38. %% @doc
  39. %% Starts the server
  40. %%
  41. %% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
  42. %% @end
  43. %%--------------------------------------------------------------------
  44. start_link(Media, To, Type, Port, CallID) ->
  45. gen_server:start_link(?MODULE, [Media, To, Type, Port, CallID], []).
  46. stop(Srv) ->
  47. gen_server:cast(Srv, stop).
  48. %%%===================================================================
  49. %%% gen_server callbacks
  50. %%%===================================================================
  51. %%--------------------------------------------------------------------
  52. %% @private
  53. %% @doc
  54. %% Initializes the server
  55. %%
  56. %% @spec init(Args) -> {ok, State} |
  57. %% {ok, State, Timeout} |
  58. %% ignore |
  59. %% {stop, Reason}
  60. %% @end
  61. %%--------------------------------------------------------------------
  62. init([Media, To, Type, Port, CallID]) ->
  63. put(callid, CallID),
  64. {MediaName, Db, Doc, Attachment, ContentType} = Media,
  65. ?LOG_START("starting a ~s stream server to provide ~s", [Type, MediaName]),
  66. case inet:getstat(Port) of
  67. {ok, _} ->
  68. process_flag(trap_exit, true),
  69. {ok, #state{
  70. db=Db
  71. ,doc=Doc
  72. ,attachment=Attachment
  73. ,media_name=MediaName
  74. ,content_type=ContentType
  75. ,lsocket=Port
  76. ,send_to=[To]
  77. ,stream_type=Type
  78. }, 0};
  79. {error, Posix} ->
  80. ?LOG("stream server failed to start; lsock ~p error ~p", [Port, Posix]),
  81. {stop, Posix}
  82. end.
  83. %%--------------------------------------------------------------------
  84. %% @private
  85. %% @doc
  86. %% Handling call messages
  87. %%
  88. %% @spec handle_call(Request, From, State) ->
  89. %% {reply, Reply, State} |
  90. %% {reply, Reply, State, Timeout} |
  91. %% {noreply, State} |
  92. %% {noreply, State, Timeout} |
  93. %% {stop, Reason, Reply, State} |
  94. %% {stop, Reason, State}
  95. %% @end
  96. %%--------------------------------------------------------------------
  97. handle_call(_Request, _From, State) ->
  98. {reply, ok, State}.
  99. %%--------------------------------------------------------------------
  100. %% @private
  101. %% @doc
  102. %% Handling cast messages
  103. %%
  104. %% @spec handle_cast(Msg, State) -> {noreply, State} |
  105. %% {noreply, State, Timeout} |
  106. %% {stop, Reason, State}
  107. %% @end
  108. %%--------------------------------------------------------------------
  109. handle_cast(stop, State) ->
  110. {stop, normal, State}.
  111. %%--------------------------------------------------------------------
  112. %% @private
  113. %% @doc
  114. %% Handling all non call/cast messages
  115. %%
  116. %% @spec handle_info(Info, State) -> {noreply, State} |
  117. %% {noreply, State, Timeout} |
  118. %% {stop, Reason, State}
  119. %% @end
  120. %%--------------------------------------------------------------------
  121. handle_info(timeout, #state{db=Db, doc=Doc, attachment=Attachment, media_name=MediaName, content_type=CType
  122. ,lsocket=LSocket, send_to=SendTo, stream_type=StreamType}=S) ->
  123. {ok, PortNo} = inet:port(LSocket),
  124. {ok, Content} = fetch_attachment(Db, Doc, Attachment),
  125. Size = byte_size(Content),
  126. ChunkSize = case ?CHUNKSIZE > Size of
  127. true -> Size;
  128. false -> ?CHUNKSIZE
  129. end,
  130. ContentType = case filename:extension(Attachment) of
  131. <<$., Ext/binary>> ->
  132. Ext;
  133. _ -> CType
  134. end,
  135. CallID = get(callid),
  136. {Resp, Header, CT, StreamUrl} = case ContentType of
  137. <<"mp3">> ->
  138. Self = self(),
  139. spawn(fun() -> put(callid, CallID), start_shout_acceptor(Self, LSocket) end),
  140. Url = list_to_binary(["shout://", net_adm:localhost(), ":", integer_to_list(PortNo), "/stream.mp3"]),
  141. {
  142. wh_shout:get_shout_srv_response(list_to_binary([?APP_NAME, ": ", ?APP_VERSION]), MediaName, ChunkSize, Url, ?CONTENT_TYPE_MP3)
  143. ,{0,wh_shout:get_shout_header(MediaName, Url)}
  144. ,?CONTENT_TYPE_MP3
  145. ,Url
  146. };
  147. <<"wav">> ->
  148. Self = self(),
  149. spawn(fun() -> put(callid, CallID), start_stream_acceptor(Self, LSocket) end),
  150. {
  151. get_http_response_headers(?CONTENT_TYPE_WAV, Size)
  152. ,undefined
  153. ,?CONTENT_TYPE_WAV
  154. ,list_to_binary(["http://", net_adm:localhost(), ":", integer_to_list(PortNo), "/stream.wav"])
  155. }
  156. end,
  157. lists:foreach(fun(To) -> send_media_resp(MediaName, StreamUrl, To) end, SendTo),
  158. MediaFile = #media_file{stream_url=StreamUrl, contents=Content, content_type=CT, media_name=MediaName, chunk_size=ChunkSize
  159. ,shout_response=Resp, shout_header=Header, continuous=(StreamType =:= continuous), pad_response=(CT =:= ?CONTENT_TYPE_MP3)},
  160. MediaLoop = spawn_link(fun() -> put(callid, CallID), play_media(MediaFile) end),
  161. {noreply, S#state{media_loop=MediaLoop, media_file=MediaFile}, hibernate};
  162. handle_info({add_listener, ListenerQ}, #state{stream_type=single, media_name=MediaName, db=Db, doc=Doc, attachment=Attachment}=S) ->
  163. CallID = get(callid),
  164. spawn(fun() ->
  165. Media = {MediaName, Db, Doc, Attachment},
  166. {ok, ShoutSrv} = media_shout_sup:start_shout(Media, ListenerQ, continuous, media_srv:next_port(), CallID),
  167. media_srv:add_stream(MediaName, ShoutSrv)
  168. end),
  169. {noreply, S, hibernate};
  170. handle_info({add_listener, ListenerQ}, #state{media_file=#media_file{stream_url=StreamUrl}, media_name=MediaName, send_to=SendTo}=S) ->
  171. send_media_resp(MediaName, StreamUrl, ListenerQ),
  172. {noreply, S#state{send_to=[ListenerQ | SendTo]}, hibernate};
  173. handle_info({send_media, Socket}, #state{media_loop=undefined, media_file=MediaFile}=S) ->
  174. CallID = get(callid),
  175. ?LOG("starting a new process to satisfy send media request"),
  176. MediaLoop = spawn_link(fun() -> put(callid, CallID), play_media(MediaFile) end),
  177. ok = gen_tcp:controlling_process(Socket, MediaLoop),
  178. MediaLoop ! {add_socket, Socket},
  179. {noreply, S#state{media_loop = MediaLoop}, hibernate};
  180. handle_info({send_media, Socket}, #state{media_loop=MediaLoop}=S) ->
  181. ok = gen_tcp:controlling_process(Socket, MediaLoop),
  182. MediaLoop ! {add_socket, Socket},
  183. {noreply, S};
  184. handle_info({'EXIT', From, ok}, #state{media_loop=MediaLoop}=S) when From =:= MediaLoop ->
  185. {stop, normal, S};
  186. handle_info({'EXIT', From, normal}, #state{media_loop=MediaLoop}=S) when From =:= MediaLoop ->
  187. {stop, normal, S};
  188. handle_info({'EXIT', From, Reason}, #state{media_loop=MediaLoop, media_file=MediaFile}=S) when From =:= MediaLoop ->
  189. CallID = get(callid),
  190. MediaLoop1 = spawn_link(fun() -> put(callid, CallID), play_media(MediaFile) end),
  191. ?LOG("media stream process ~p unexpectly died ~s, restarting", [From, Reason]),
  192. {noreply, S#state{media_loop = MediaLoop1}, hibernate};
  193. handle_info(_Info, State) ->
  194. {noreply, State}.
  195. %%--------------------------------------------------------------------
  196. %% @private
  197. %% @doc
  198. %% This function is called by a gen_server when it is about to
  199. %% terminate. It should be the opposite of Module:init/1 and do any
  200. %% necessary cleaning up. When it returns, the gen_server terminates
  201. %% with Reason. The return value is ignored.
  202. %%
  203. %% @spec terminate(Reason, State) -> void()
  204. %% @end
  205. %%--------------------------------------------------------------------
  206. terminate(_Reason, #state{lsocket=undefined}) ->
  207. ?LOG_END("stream server ~s termination", [_Reason]);
  208. terminate(_Reason, #state{lsocket=LSock}) ->
  209. {ok, PortNo} = inet:port(LSock),
  210. ?LOG("closing port ~b", [PortNo]),
  211. gen_tcp:close(LSock),
  212. ?LOG_END("stream server ~s termination", [_Reason]).
  213. %%--------------------------------------------------------------------
  214. %% @private
  215. %% @doc
  216. %% Convert process state when code is changed
  217. %%
  218. %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
  219. %% @end
  220. %%--------------------------------------------------------------------
  221. code_change(_OldVsn, State, _Extra) ->
  222. {ok, State}.
  223. %%%===================================================================
  224. %%% Internal functions
  225. %%%===================================================================
  226. send_media_resp(MediaName, Url, To) ->
  227. Prop = [{<<"Media-Name">>, MediaName}
  228. ,{<<"Stream-URL">>, Url}
  229. | wh_api:default_headers(<<>>, <<"media">>, <<"media_resp">>, ?APP_NAME, ?APP_VERSION)],
  230. {ok, JSON} = wh_api:media_resp(Prop),
  231. ?LOG("notifying requestor that ~s as available at ~s", [MediaName, Url]),
  232. amqp_util:targeted_publish(To, JSON).
  233. start_shout_acceptor(Parent, LSock) ->
  234. {ok, PortNo} = inet:port(LSock),
  235. ?LOG_START("shout acceptor listening on ~b for client connections", [PortNo]),
  236. case gen_tcp:accept(LSock) of
  237. {ok, S} ->
  238. CallID = get(callid),
  239. spawn(fun() -> put(callid, CallID), start_shout_acceptor(Parent, LSock) end),
  240. case wh_shout:get_request(S) of
  241. void ->
  242. ?LOG_END("recieved invalid client request"),
  243. gen_tcp:close(S);
  244. _ ->
  245. ?LOG_END("new client connected"),
  246. ok = gen_tcp:controlling_process(S, Parent),
  247. Parent ! {send_media, S}
  248. end;
  249. _ -> ok
  250. end.
  251. start_stream_acceptor(Parent, LSock) ->
  252. {ok, PortNo} = inet:port(LSock),
  253. ?LOG_START("raw acceptor listening on ~b for client connections", [PortNo]),
  254. case gen_tcp:accept(LSock) of
  255. {ok, S} ->
  256. CallID = get(callid),
  257. spawn(fun() -> put(callid, CallID), start_stream_acceptor(Parent, LSock) end),
  258. {ok, {Address, Port}} = inet:peername(S),
  259. ?LOG_END("client connected from ~s:~b", [inet_parse:ntoa(Address), Port]),
  260. _Req = wh_shout:get_request(S),
  261. ok = gen_tcp:controlling_process(S, Parent),
  262. Parent ! {send_media, S};
  263. _ -> ok
  264. end.
  265. play_media(#media_file{contents=Contents, shout_header=Header}=MediaFile) ->
  266. play_media(MediaFile, [], 0, byte_size(Contents), <<>>, Header).
  267. play_media(#media_file{shout_response=ShoutResponse, shout_header=ShoutHeader}=MediaFile, [], _, Stop, _, _) ->
  268. ?LOG_START("started new process to stream media to client"),
  269. receive
  270. {add_socket, S} ->
  271. ?LOG("started stream"),
  272. ok = gen_tcp:send(S, [ShoutResponse]),
  273. play_media(MediaFile, [S], 0, Stop, <<>>, ShoutHeader);
  274. shutdown ->
  275. ?LOG_END("stream shutdown")
  276. after ?MAX_WAIT_FOR_LISTENERS ->
  277. ?LOG_END("stream stood up waiting for connection, outta here")
  278. end;
  279. play_media(#media_file{continuous=Continuous, shout_response=ShoutResponse, shout_header=ShoutHeader}=MediaFile
  280. ,Socks, Offset, Stop, SoFar, Header) ->
  281. receive
  282. {add_socket, S} ->
  283. ok = gen_tcp:send(S, [ShoutResponse]),
  284. play_media(MediaFile, [S | Socks], Offset, Stop, SoFar, Header);
  285. shutdown ->
  286. ?LOG_END("stream shutdown")
  287. after 0 ->
  288. ?LOG("playing at offset ~p", [Offset]),
  289. case wh_shout:play_chunk(MediaFile, Socks, Offset, Stop, SoFar, Header) of
  290. {Socks1, Header1, Offset1, SoFar1} ->
  291. ?LOG("continue playing at offset ~p", [Offset1]),
  292. play_media(MediaFile, Socks1, Offset1, Stop, SoFar1, Header1);
  293. {done, Socks1} ->
  294. case Continuous of
  295. true ->
  296. ?LOG("end of stream, looping"),
  297. play_media(MediaFile, Socks1, 0, Stop, <<>>, ShoutHeader);
  298. false ->
  299. ?LOG_END("end of stream"),
  300. [gen_tcp:close(S) || S <- Socks1]
  301. end
  302. end
  303. end.
  304. get_http_response_headers(CT, CL) ->
  305. ["HTTP/1.1 200 OK\r\n"
  306. ,"Server: ", ?APP_NAME, "/", ?APP_VERSION, "\r\n"
  307. ,"Content-Type: ", wh_util:to_list(CT), "\r\n"
  308. ,"Content-Disposition: identity\r\n"
  309. ,"Content-Length: ", wh_util:to_list(CL), "\r\n\r\n"].
  310. -spec fetch_attachment/3 :: (binary(), binary(), binary()) -> {'ok', binary()} | {'error', 'timeout'}.
  311. fetch_attachment(Db, Doc, Attachment) ->
  312. fetch_attachment(Db, Doc, Attachment, 0).
  313. fetch_attachment(_DB, _Doc, _A, ?MAX_FETCH_RETRIES) ->
  314. ?LOG_SYS("Failed to retrieve attachment"),
  315. ?LOG_SYS("DB: ~s", [_DB]),
  316. ?LOG_SYS("Doc: ~s", [_Doc]),
  317. ?LOG_SYS("Attachment: ~s", [_A]),
  318. {error, timeout};
  319. fetch_attachment(Db, Doc, Attachment, Retries) ->
  320. case couch_mgr:fetch_attachment(Db, Doc, Attachment) of
  321. {ok, _Content}=OK -> OK;
  322. {error, _Err} ->
  323. ?LOG_SYS("Error getting attachment: ~s", [_Err]),
  324. timer:sleep(100 * Retries),
  325. fetch_attachment(Db, Doc, Attachment, Retries+1)
  326. end.