Packages

Collection of functions for working with Cloudflare Stream API

Current section

Files

Jump to
cloudflare_stream lib cloudflare_stream.ex
Raw

lib/cloudflare_stream.ex

defmodule CloudflareStream do
require Logger
@opts_keys %{
# media
"TYPE" => :type,
"GROUP-ID" => :group_id,
"NAME" => :name,
"LANGUAGE" => :language,
"DEFAULT" => :default,
"AUTOSELECT" => :autoselect,
"URI" => :uri,
# extension
"EXT" => :extension,
# track info
"RESOLUTION" => :resolution,
"CODECS" => :codecs,
"BANDWIDTH" => :bandwidth,
"FRAME-RATE" => :frame_rate,
"AUDIO" => :audio
}
@opts_types %{
codecs: :list,
autoselect: :boolean,
default: :boolean,
bandwidth: :integer
}
@track_key :track
@doc """
Example usage:
```
CloudflareStream.parse_metadata(metadata, :m3u)
```
Extracts all available data from M3U video metadata
Example .m3u8 metadata:
```
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio",NAME="eng",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="stream_tf1bea3e54501931464e543bb005e9d0d_r15218387.m3u8"
#EXT-X-STREAM-INF:RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2",BANDWIDTH=3728000,FRAME-RATE=30.000,AUDIO="group_audio"
stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218383.m3u8
#EXT-X-STREAM-INF:RESOLUTION=1920x1080,CODECS="avc1.4d4028,mp4a.40.2",BANDWIDTH=5328000,FRAME-RATE=30.000,AUDIO="group_audio"
stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218385.m3u8
#EXT-X-STREAM-INF:RESOLUTION=854x480,CODECS="avc1.4d401f,mp4a.40.2",BANDWIDTH=1928000,FRAME-RATE=30.000,AUDIO="group_audio"
stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218382.m3u8
#EXT-X-STREAM-INF:RESOLUTION=640x360,CODECS="avc1.4d401e,mp4a.40.2",BANDWIDTH=928000,FRAME-RATE=30.000,AUDIO="group_audio"
stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218381.m3u8
#EXT-X-STREAM-INF:RESOLUTION=426x240,CODECS="avc1.42c015,mp4a.40.2",BANDWIDTH=528000,FRAME-RATE=30.000,AUDIO="group_audio"
stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218380.m3u8
```
Parse result:
```
%{
extension: "M3U",
media: %{
autoselect: true,
default: true,
group_id: "group_audio",
language: "en",
name: "eng",
type: "AUDIO",
uri: "stream_tf1bea3e54501931464e543bb005e9d0d_r15218387.m3u8"
},
tracks: [
%{
audio: "group_audio",
bandwidth: 3728000,
codecs: ["avc1.4d401f", "mp4a.40.2"],
frame_rate: "30.000",
resolution: "1280x720",
uri: "stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218383.m3u8"
},
%{
audio: "group_audio",
bandwidth: 5328000,
codecs: ["avc1.4d4028", "mp4a.40.2"],
frame_rate: "30.000",
resolution: "1920x1080",
uri: "stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218385.m3u8"
},
%{
audio: "group_audio",
bandwidth: 1928000,
codecs: ["avc1.4d401f", "mp4a.40.2"],
frame_rate: "30.000",
resolution: "854x480",
uri: "stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218382.m3u8"
},
%{
audio: "group_audio",
bandwidth: 928000,
codecs: ["avc1.4d401e", "mp4a.40.2"],
frame_rate: "30.000",
resolution: "640x360",
uri: "stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218381.m3u8"
},
%{
audio: "group_audio",
bandwidth: 528000,
codecs: ["avc1.42c015", "mp4a.40.2"],
frame_rate: "30.000",
resolution: "426x240",
uri: "stream_ta2d3c5d56a9f8537daa3b2b7ddabd5b0_r15218380.m3u8"
}
],
version: "6"
}
```
"""
@spec parse_metadata(String.t(), :m3u) :: map()
def parse_metadata(metadata, :m3u) do
split =
metadata
|> String.split("\n")
split
|> Enum.map(fn x -> parse_line(x, metadata) end)
|> parse_format(split)
|> format()
end
defp format(opts) do
opts
|> Enum.filter(&is_map/1)
|> collect_keys(@track_key, :tracks)
|> Enum.reduce(%{}, fn x, acc ->
Map.merge(acc, x)
end)
end
defp collect_keys(opts, key, put_key) do
collected =
opts
|> Enum.filter(fn x ->
Map.get(x, key) != nil
end)
|> Enum.reduce(%{put_key => []}, fn x, acc ->
v = Map.get(x, key)
Map.put(acc, put_key, acc[put_key] ++ [v])
end)
opts
|> Enum.reject(fn x ->
Map.get(x, key) != nil
end)
|> List.insert_at(-1, collected)
end
defp opts_atom_key(key) do
@opts_keys
|> Map.get(key, key)
end
defp parse_format(x, [h | _t]) when is_binary(h) do
format =
h
|> String.split("#EXT")
|> List.last()
format_opts =
"EXT"
|> opts_atom_key()
|> List.wrap()
|> List.insert_at(1, format)
|> List.to_tuple()
|> List.wrap()
|> Map.new()
|> List.wrap()
format_opts ++ x
end
defp parse_line("#EXT-X-VERSION:" <> opts, _) do
opts
|> version()
end
defp parse_line("#EXT-X-MEDIA:" <> opts, _) do
opts
|> fragment("URI")
|> fragment("LANGUAGE")
|> fragment("GROUP-ID")
|> fragment("NAME")
|> extract_opts([:media])
end
defp parse_line("#EXT-X-STREAM-INF:" <> opts, raw) do
opts
|> uri(raw)
|> fragment("CODECS")
|> fragment("AUDIO")
|> extract_opts([@track_key])
end
defp parse_line("stream_" <> _track_path = x, _) do
x
end
defp parse_line(x, _) do
Logger.debug("Ignored metadata string >> #{x}")
x
end
defp uri(line, raw) do
uri =
raw
|> String.split(line)
|> List.last()
|> String.split("\n")
|> Enum.filter(fn x -> String.length(x) > 0 end)
|> List.first()
line
|> String.replace_suffix("", "," <> "URI=#{uri}")
end
defp fragment(line, key) do
fragment =
line
|> String.split("#{key}=\"")
|> List.last()
|> String.split("\"")
|> List.first()
new_fragment =
fragment
|> String.replace(",", ";", [])
replace_fragment =
fragment
|> String.replace_prefix("", "\"")
|> String.replace_suffix("", "\"")
line
|> String.replace(replace_fragment, new_fragment)
end
defp version(version) do
%{version: version}
end
defp extract_opts(opts, path) when is_list(path) do
opts
|> String.split(",")
|> Enum.flat_map(fn x ->
String.split(x, "=")
end)
|> Enum.chunk_every(2)
|> parse_opts(path)
end
defp parse_opts(opts, path) do
opts
|> Enum.map(&List.to_tuple/1)
|> Enum.reduce(%{}, fn ({key, value}, acc) ->
key = opts_atom_key(key)
value = parse_opt_value(key, value)
put_opts(acc, path ++ [key], value)
end)
end
defp parse_opt_value(key, value) do
case Map.get(@opts_types, key) do
:list -> String.split(value, ";")
:boolean -> bool_value(value)
:integer -> integer_value(value)
_ -> value
end
end
defp integer_value(value) do
case Integer.parse(value) do
{x, _} -> x
_ -> value
end
end
defp bool_value("YES"), do: true
defp bool_value(_), do: false
defp put_opts(opts, path, value) do
last = List.last(path)
path
|> Enum.reduce(opts, fn (x, acc) ->
acc =
case Map.get(acc, x) do
nil when last != x -> put_in(acc, [x], %{})
_ -> acc
end
case x do
k when k == last -> put_in(acc, path, value)
_ -> acc
end
end)
end
end