├── config ├── dev.exs ├── production.exs ├── config.exs └── test.exs ├── test ├── test_helper.exs ├── mux │ ├── base_test.exs │ ├── data │ │ ├── errors_test.exs │ │ ├── exports_test.exs │ │ ├── filters_test.exs │ │ ├── dimensions_test.exs │ │ ├── video_views_test.exs │ │ ├── incidents_test.exs │ │ ├── metrics_test.exs │ │ └── monitoring_test.exs │ ├── video │ │ ├── playback_ids_test.exs │ │ ├── delivery_usage_test.exs │ │ ├── tracks_test.exs │ │ ├── signing_keys_test.exs │ │ ├── uploads_test.exs │ │ ├── playback_restriction_test.exs │ │ ├── transcription_vocabularies_test.exs │ │ ├── spaces_test.exs │ │ ├── live_streams_test.exs │ │ └── assets_test.exs │ ├── webhooks_test.exs │ └── token_test.exs └── mux_test.exs ├── logo.png ├── .travis.yml ├── CODEOWNERS ├── github-elixir-sdk.png ├── .formatter.exs ├── lib ├── mux │ ├── exception.ex │ ├── video │ │ ├── delivery_usage.ex │ │ ├── playback_ids.ex │ │ ├── tracks.ex │ │ ├── signing_keys.ex │ │ ├── uploads.ex │ │ ├── playback_restrictions.ex │ │ ├── transcription_vocabularies.ex │ │ ├── spaces.ex │ │ ├── assets.ex │ │ └── live_streams.ex │ ├── data │ │ ├── errors.ex │ │ ├── dimensions.ex │ │ ├── exports.ex │ │ ├── filters.ex │ │ ├── video_views.ex │ │ ├── incidents.ex │ │ ├── monioring.ex │ │ ├── metrics.ex │ │ └── real_time.ex │ ├── tesla.ex │ ├── webhooks │ │ └── test_utils.ex │ ├── base.ex │ ├── webhooks.ex │ ├── token.ex │ └── support │ │ └── fixtures.ex └── mux.ex ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── mix.exs ├── README.md └── mix.lock /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/production.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxinc/mux-elixir/HEAD/logo.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: '1.6' 3 | otp_release: '20.0' 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @muxinc/techops 2 | /CODEOWNERS @muxinc/platform-engineering 3 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | import_config "#{Mix.env()}.exs" 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :tesla, adapter: Tesla.Mock 4 | -------------------------------------------------------------------------------- /github-elixir-sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxinc/mux-elixir/HEAD/github-elixir-sdk.png -------------------------------------------------------------------------------- /test/mux/base_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.BaseTest do 2 | use ExUnit.Case 3 | doctest Mux.Base 4 | end 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/mux_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MuxTest do 2 | use ExUnit.Case 3 | doctest Mux 4 | 5 | test "creates a new connection client" do 6 | client = Mux.client("abcd", "abcd1234") 7 | assert %Tesla.Client{} = client 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mux/exception.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Exception do 2 | @moduledoc false 3 | defexception [:message, :response] 4 | 5 | def exception(%{status: status} = resp) do 6 | msg = "An unexpected error occurred with status code: #{status}" 7 | %__MODULE__{message: msg, response: resp} 8 | end 9 | 10 | def exception(error) do 11 | %__MODULE__{message: "An unexpected error occurred.", response: error} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/mux/data/errors_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.ErrorsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Errors 5 | 6 | @base_url "https://api.mux.com/data/v1/errors" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.errors()} 14 | end) 15 | 16 | {:ok, %{client: client}} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/mux/data/exports_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.ExportsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Exports 5 | 6 | @base_url "https://api.mux.com/data/v1/exports" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.exports()} 14 | 15 | %{method: :get, url: @base_url <> "/views"} -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.view_exports()} 17 | end) 18 | 19 | {:ok, %{client: client}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/mux/data/filters_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.FiltersTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Filters 5 | 6 | @base_url "https://api.mux.com/data/v1/filters" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.filters()} 14 | 15 | %{method: :get, url: @base_url <> "/browser"} -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.filters(:browser)} 17 | end) 18 | 19 | {:ok, %{client: client}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/mux/data/dimensions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.DimensionsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Dimensions 5 | 6 | @base_url "https://api.mux.com/data/v1/dimensions" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.dimensions()} 14 | 15 | %{method: :get, url: @base_url <> "/browser"} -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.dimensions(:browser)} 17 | end) 18 | 19 | {:ok, %{client: client}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/mux/data/video_views_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.VideoViewsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.VideoViews 5 | 6 | @base_url "https://api.mux.com/data/v1/video-views" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.video_views()} 14 | 15 | %{method: :get, url: @base_url <> "/k8n4aklUyrRDekILDWta1qSJqNFpYB7N50"} -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.video_view()} 17 | end) 18 | 19 | {:ok, %{client: client}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/mux/video/playback_ids_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.PlaybackIdsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.PlaybackIds 5 | 6 | @base_url "https://api.mux.com/video/v1/playback-ids" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url <> "/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE"} -> 13 | %Tesla.Env{ 14 | status: 200, 15 | body: %{ 16 | "data" => Mux.Fixtures.playback_id_full() 17 | } 18 | } 19 | end) 20 | 21 | {:ok, %{client: client}} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/mux/video/delivery_usage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.DeliveryUsageTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.DeliveryUsage 5 | 6 | @base_url "https://api.mux.com/video/v1/delivery-usage" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url, query: %{timeframe: [1_564_617_600, 1_569_283_200]}} -> 13 | %Tesla.Env{ 14 | status: 200, 15 | body: %{ 16 | "data" => [Mux.Fixtures.delivery_usage(), Mux.Fixtures.delivery_usage()] 17 | } 18 | } 19 | end) 20 | 21 | {:ok, %{client: client}} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.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 | # ElixirLS artifacts should not be under source control. 14 | /.elixir_ls/ 15 | 16 | # Ignore .fetch files in case you like to edit your project deps locally. 17 | /.fetch 18 | 19 | # If the VM crashes, it generates a dump, let's ignore it too. 20 | erl_crash.dump 21 | 22 | # Also ignore archive artifacts (built via "mix archive.build"). 23 | *.ez 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | mux_ex-*.tar -------------------------------------------------------------------------------- /test/mux/data/incidents_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.IncidentsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Incidents 5 | 6 | @base_url "https://api.mux.com/data/v1/incidents" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{status: 200, body: Mux.Fixtures.incidents()} 14 | 15 | %{method: :get, url: @base_url <> "/ABCD1234"} -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.incident()} 17 | 18 | %{method: :get, url: @base_url <> "/ABCD1234/related"} -> 19 | %Tesla.Env{status: 200, body: Mux.Fixtures.related_incidents()} 20 | end) 21 | 22 | {:ok, %{client: client}} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | env: 9 | MIX_ENV: test 10 | 11 | jobs: 12 | elixir-tests: 13 | name: Elixir Tests 14 | timeout-minutes: 30 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Elixir 19 | uses: erlef/setup-elixir@v1 20 | with: 21 | elixir-version: "1.6" # Define the elixir version [required] 22 | otp-version: "20.0" # Define the OTP version [required] 23 | - name: Restore deps cache 24 | uses: actions/cache@v2 25 | with: 26 | path: deps 27 | key: ${{ runner.os }}-deps-v1-${{ hashFiles('**/mix.lock') }} 28 | restore-keys: ${{ runner.os }}-deps-v1- 29 | - name: Install dependencies 30 | run: mix deps.get 31 | - name: Run tests 32 | run: mix test 33 | -------------------------------------------------------------------------------- /lib/mux/video/delivery_usage.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.DeliveryUsage do 2 | @moduledoc """ 3 | This module provides functions for reading delivery usage in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/delivery-usage) 4 | """ 5 | alias Mux.{Base, Fixtures} 6 | 7 | @path "/video/v1/delivery-usage" 8 | 9 | @doc """ 10 | Get delivery usage for a specified timeframe. [API Documentation](https://docs.mux.com/api-reference/video#operation/list-delivery-usage) 11 | 12 | Returns `{:ok, delivery_usage, %Tesla.Env{}}`. 13 | 14 | ## Examples 15 | 16 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 17 | iex> {:ok, delivery_usage, _env} = Mux.Video.DeliveryUsage.list(client, %{timeframe: [1564617600, 1569283200]}) 18 | iex> delivery_usage 19 | #{inspect([Fixtures.delivery_usage(), Fixtures.delivery_usage()])} 20 | """ 21 | 22 | def list(client, options \\ []) do 23 | Base.get(client, @path, query: options) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/mux/video/tracks_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.TracksTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.Tracks 5 | 6 | @base_url "https://api.mux.com/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/tracks" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :post, url: @base_url} -> 13 | %Tesla.Env{ 14 | status: 201, 15 | body: %{ 16 | "data" => Mux.Fixtures.track() 17 | } 18 | } 19 | 20 | %{method: :get, url: @base_url <> "/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE"} -> 21 | %Tesla.Env{ 22 | status: 200, 23 | body: %{ 24 | "data" => Mux.Fixtures.track() 25 | } 26 | } 27 | 28 | %{method: :delete, url: @base_url <> "/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE"} -> 29 | %Tesla.Env{status: 204, body: ""} 30 | end) 31 | 32 | {:ok, %{client: client}} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mux/video/playback_ids.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.PlaybackIds do 2 | @moduledoc """ 3 | This module provides functions around managing Playback IDs in Mux Video. Playback IDs are the 4 | public identifier for streaming a piece of content and can include policies such as `signed` or 5 | `public`. [API Documentation](https://docs.mux.com/api-reference/video#tag/playback-id). 6 | """ 7 | alias Mux.{Base, Fixtures} 8 | 9 | @path "/video/v1/playback-ids" 10 | 11 | @doc """ 12 | Retrieve a asset or live stream identifier by Playback ID. 13 | 14 | Returns `{:ok, playback_id_full, raw_env}`. 15 | 16 | ## Examples 17 | 18 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 19 | iex> {:ok, playback_id_full, _env} = Mux.Video.PlaybackIds.get(client, "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 20 | iex> playback_id_full 21 | #{inspect(Fixtures.playback_id_full())} 22 | 23 | """ 24 | def get(client, playback_id) do 25 | Base.get(client, "#{@path}/#{playback_id}") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Mux, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /lib/mux/data/errors.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Errors do 2 | @moduledoc """ 3 | This module provides functions for working with playback errors, which is typically an error 4 | thrown by the player that caused playback failure. [API Documentation](https://docs.mux.com/api-reference/data#tag/errors). 5 | """ 6 | alias Mux.{Base, Fixtures} 7 | 8 | @doc """ 9 | Returns a list of playback errors along with details and statistics about them. 10 | 11 | Returns `{:ok, errors, raw_env}`. 12 | 13 | ## Examples 14 | 15 | iex> client = Mux.client("my_token_id", "my_token_secret") 16 | iex> {:ok, errors, _env} = Mux.Data.Errors.list(client) 17 | iex> errors 18 | #{inspect(Fixtures.errors()["data"])} 19 | 20 | iex> client = Mux.client("my_token_id", "my_token_secret") 21 | iex> {:ok, errors, _env} = Mux.Data.Errors.list(client, filters: ["operating_system:windows"], timeframe: ["24:hours"]) 22 | iex> errors 23 | #{inspect(Fixtures.errors()["data"])} 24 | 25 | """ 26 | def list(client, params \\ []) do 27 | Base.get(client, build_base_path(), query: params) 28 | end 29 | 30 | defp build_base_path(), do: "/data/v1/errors" 31 | end 32 | -------------------------------------------------------------------------------- /test/mux/video/signing_keys_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.SigningKeysTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.SigningKeys 5 | 6 | @base_url "https://api.mux.com/video/v1/signing-keys/" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{ 14 | status: 200, 15 | body: %{ 16 | "data" => [Mux.Fixtures.signing_key(), Mux.Fixtures.signing_key()] 17 | } 18 | } 19 | 20 | %{method: :post, url: @base_url} -> 21 | %Tesla.Env{ 22 | status: 201, 23 | body: %{ 24 | "data" => Mux.Fixtures.signing_key(:create) 25 | } 26 | } 27 | 28 | %{method: :get, url: @base_url <> "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C"} -> 29 | %Tesla.Env{ 30 | status: 200, 31 | body: %{ 32 | "data" => Mux.Fixtures.signing_key() 33 | } 34 | } 35 | 36 | %{method: :delete, url: @base_url <> "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C"} -> 37 | %Tesla.Env{status: 204, body: ""} 38 | end) 39 | 40 | {:ok, %{client: client}} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/mux/video/uploads_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.UploadsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.Uploads 5 | 6 | @base_url "https://api.mux.com/video/v1/uploads/" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{ 14 | status: 200, 15 | body: %{ 16 | "data" => [Mux.Fixtures.upload(), Mux.Fixtures.upload()] 17 | } 18 | } 19 | 20 | %{method: :post, url: @base_url} -> 21 | %Tesla.Env{ 22 | status: 201, 23 | body: %{ 24 | "data" => Mux.Fixtures.upload(:create) 25 | } 26 | } 27 | 28 | %{method: :get, url: @base_url <> "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM"} -> 29 | %Tesla.Env{ 30 | status: 200, 31 | body: %{ 32 | "data" => Mux.Fixtures.upload() 33 | } 34 | } 35 | 36 | %{method: :put, url: @base_url <> "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM/cancel"} -> 37 | %Tesla.Env{status: 200, body: %{"data" => Mux.Fixtures.upload(:cancel)}} 38 | end) 39 | 40 | {:ok, %{client: client}} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mux/data/dimensions.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Dimensions do 2 | @moduledoc """ 3 | This module includes functions for retrieving available dimensions and their values in our system. These 4 | endpoints, for example, are used to construct the breakdown tables in the metrics UI. [API Documentation](https://docs.mux.com/api-reference/data#tag/dimensions) 5 | """ 6 | alias Mux.{Base, Fixtures} 7 | 8 | @doc """ 9 | Lists all the dimensions broken out into basic and advanced. 10 | 11 | Returns `{:ok, dimensions, raw_env}`. 12 | 13 | ## Examples 14 | 15 | iex> client = Mux.client("my_token_id", "my_token_secret") 16 | iex> {:ok, dimensions, _env} = Mux.Data.Dimensions.list(client) 17 | iex> dimensions 18 | #{inspect(Fixtures.dimensions()["data"])} 19 | 20 | """ 21 | def list(client) do 22 | Base.get(client, build_base_path()) 23 | end 24 | 25 | @doc """ 26 | Lists the values for a specific dimension along with a total count of related views. 27 | 28 | Returns `{:ok, dimensions, raw_env}`. 29 | 30 | ## Examples 31 | 32 | iex> client = Mux.client("my_token_id", "my_token_secret") 33 | iex> {:ok, dimensions, _env} = Mux.Data.Dimensions.get(client, "browser") 34 | iex> dimensions 35 | #{inspect(Fixtures.dimensions(:browser)["data"])} 36 | 37 | """ 38 | def get(client, value, params \\ []) do 39 | Base.get(client, build_base_path() <> "/#{value}", query: params) 40 | end 41 | 42 | defp build_base_path(), do: "/data/v1/dimensions" 43 | end 44 | -------------------------------------------------------------------------------- /lib/mux/tesla.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Tesla do 2 | @moduledoc false 3 | 4 | use Tesla 5 | 6 | plug(Tesla.Middleware.JSON) 7 | plug(Tesla.Middleware.Headers, [{"user-agent", "Mux Elixir | 3.2.2"}]) 8 | 9 | @defaults [ 10 | base_url: "https://api.mux.com" 11 | ] 12 | 13 | @doc """ 14 | Creates a new connection client. 15 | 16 | Returns `%Tesla.Client{}`. 17 | 18 | ## Examples 19 | 20 | iex> Mux.Base.new("my_token_id", "my_token_secret") 21 | %Tesla.Client{ 22 | fun: nil, 23 | post: [], 24 | pre: [ 25 | {Tesla.Middleware.BaseUrl, :call, ["https://api.mux.com"]}, 26 | {Tesla.Middleware.BasicAuth, :call, 27 | [%{password: "my_token_secret", username: "my_token_id"}]} 28 | ] 29 | } 30 | 31 | iex> Mux.Base.new("my_token_id", "my_token_secret", base_url: "https://staging.mux.com") 32 | %Tesla.Client{ 33 | fun: nil, 34 | post: [], 35 | pre: [ 36 | {Tesla.Middleware.BaseUrl, :call, ["https://staging.mux.com"]}, 37 | {Tesla.Middleware.BasicAuth, :call, 38 | [%{password: "my_token_secret", username: "my_token_id"}]} 39 | ] 40 | } 41 | 42 | """ 43 | def new(token_id, token_secret, opts \\ []) do 44 | opts = Keyword.merge(@defaults, opts) |> Enum.into(%{}) 45 | 46 | Tesla.client([ 47 | {Tesla.Middleware.BaseUrl, opts.base_url}, 48 | {Tesla.Middleware.BasicAuth, %{username: token_id, password: token_secret}} 49 | ]) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/mux/data/metrics_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.MetricsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Metrics 5 | 6 | @base_url "https://api.mux.com/data/v1/metrics" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{ 13 | method: :get, 14 | url: @base_url <> "/video_startup_time/breakdown", 15 | query: [group_by: "browser"] 16 | } -> 17 | %Tesla.Env{status: 200, body: Mux.Fixtures.breakdown()} 18 | 19 | %{ 20 | method: :get, 21 | url: @base_url <> "/video_startup_time/breakdown", 22 | query: [group_by: "browser", measurement: "median", timeframe: ["6:hours"]] 23 | } -> 24 | %Tesla.Env{status: 200, body: Mux.Fixtures.breakdown()} 25 | 26 | %{ 27 | method: :get, 28 | url: @base_url <> "/comparison", 29 | query: [dimension: "browser", value: "Safari"] 30 | } -> 31 | %Tesla.Env{status: 200, body: Mux.Fixtures.comparison()} 32 | 33 | %{method: :get, url: @base_url <> "/video_startup_time/insights"} -> 34 | %Tesla.Env{status: 200, body: Mux.Fixtures.insights()} 35 | 36 | %{method: :get, url: @base_url <> "/video_startup_time/overall"} -> 37 | %Tesla.Env{status: 200, body: Mux.Fixtures.overall()} 38 | 39 | %{method: :get, url: @base_url <> "/video_startup_time/timeseries"} -> 40 | %Tesla.Env{status: 200, body: Mux.Fixtures.timeseries()} 41 | end) 42 | 43 | {:ok, %{client: client}} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/mux/webhooks_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.WebhooksTest do 2 | use ExUnit.Case 3 | 4 | @payload "{\"test\":\"body\"}" 5 | @secret "SuperSecret123" 6 | @valid_signature_at_the_time "t=1565125718,v1=854ece4c22acef7c66b57d4e504153bc512595e8e9c772ece2a68150548c19a7" 7 | 8 | describe "verify_header/3" do 9 | test "returns :error with the correct message if the signature is valid but outside the timeframe" do 10 | assert {:error, message} = 11 | Mux.Webhooks.verify_header(@payload, @valid_signature_at_the_time, @secret) 12 | 13 | assert message === "Timestamp outside the tolerance zone" 14 | end 15 | 16 | test "returns :error with the correct message if the signature is invalid" do 17 | assert {:error, message} = 18 | Mux.Webhooks.verify_header(@payload, "invalid-signature", @secret) 19 | 20 | assert message === "Unable to extract timestamp and signatures from header" 21 | end 22 | 23 | test "returns :error with the correct message if the payload doesn't have a matching signature" do 24 | payload = "{\"test\": \"some other body\"}" 25 | signature = Mux.Webhooks.TestUtils.generate_signature(payload, @secret) 26 | assert {:error, message} = Mux.Webhooks.verify_header(@payload, signature, @secret) 27 | assert message === "No signatures found matching the expected signature for payload" 28 | end 29 | 30 | test "returns :ok if the signature is valid" do 31 | signature = Mux.Webhooks.TestUtils.generate_signature(@payload, @secret) 32 | assert :ok = Mux.Webhooks.verify_header(@payload, signature, @secret) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mux/data/exports.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Exports do 2 | @moduledoc """ 3 | This module provides functions for accessing lists of data exports. Currently the only exports 4 | available are raw video views. [API Documentation](https://docs.mux.com/api-reference/data#tag/exports) 5 | 6 | **Note:** These endpoints require that your plan has access to specific features. [Reach out to us](mailto:help@mux.com) 7 | if you have questions. 8 | """ 9 | alias Mux.{Base, Fixtures} 10 | 11 | @doc """ 12 | Lists the available video view exports along with URLs to retrieve them. 13 | 14 | This method has been deprecated in favor of `Data.Exports.list_view_exports`. 15 | 16 | Returns `{:ok, exports, raw_env}`. 17 | 18 | ## Examples 19 | 20 | iex> client = Mux.client("my_token_id", "my_token_secret") 21 | iex> {:ok, exports, _env} = Mux.Data.Exports.list(client) 22 | iex> exports 23 | #{inspect(Fixtures.exports()["data"])} 24 | 25 | """ 26 | def list(client) do 27 | Base.get(client, build_base_path()) 28 | end 29 | 30 | @doc """ 31 | Lists the available video view exports along with URLs to retrieve them. 32 | 33 | Returns `{:ok, exports, raw_env}`. 34 | 35 | ## Examples 36 | 37 | iex> client = Mux.client("my_token_id", "my_token_secret") 38 | iex> {:ok, exports, _env} = Mux.Data.Exports.list_view_exports(client) 39 | iex> exports 40 | #{inspect(Fixtures.view_exports()["data"])} 41 | 42 | """ 43 | 44 | def list_view_exports(client) do 45 | Base.get(client, build_base_path() <> "/views") 46 | end 47 | 48 | defp build_base_path(), do: "/data/v1/exports" 49 | end 50 | -------------------------------------------------------------------------------- /lib/mux/data/filters.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Filters do 2 | @moduledoc """ 3 | This module includes functions for retrieving available filters and their values in our system. These 4 | endpoints, for example, are used to construct the breakdown tables in the metrics UI. [API Documentation](https://docs.mux.com/api-reference/data#tag/filters) 5 | 6 | This module has been deprecated in favor of `Data.Dimensions`. 7 | """ 8 | alias Mux.{Base, Fixtures} 9 | 10 | @doc """ 11 | Lists all the filters broken out into basic and advanced. 12 | 13 | This method has been deprecated in favor of `Data.Dimensions.list`. 14 | 15 | Returns `{:ok, filters, raw_env}`. 16 | 17 | ## Examples 18 | 19 | iex> client = Mux.client("my_token_id", "my_token_secret") 20 | iex> {:ok, filters, _env} = Mux.Data.Filters.list(client) 21 | iex> filters 22 | #{inspect(Fixtures.filters()["data"])} 23 | 24 | """ 25 | def list(client) do 26 | Base.get(client, build_base_path()) 27 | end 28 | 29 | @doc """ 30 | Lists the values for a specific filter along with a total count of related views. 31 | 32 | This method has been deprecated in favor of `Data.Dimensions.get`. 33 | 34 | Returns `{:ok, filters, raw_env}`. 35 | 36 | ## Examples 37 | 38 | iex> client = Mux.client("my_token_id", "my_token_secret") 39 | iex> {:ok, filters, _env} = Mux.Data.Filters.get(client, "browser") 40 | iex> filters 41 | #{inspect(Fixtures.filters(:browser)["data"])} 42 | 43 | """ 44 | def get(client, value, params \\ []) do 45 | Base.get(client, build_base_path() <> "/#{value}", query: params) 46 | end 47 | 48 | defp build_base_path(), do: "/data/v1/filters" 49 | end 50 | -------------------------------------------------------------------------------- /test/mux/data/monitoring_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.MonitoringTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Data.Monitoring 5 | 6 | @base_url "https://api.mux.com/data/v1/monitoring" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret") 10 | 11 | mock(fn 12 | %{ 13 | method: :get, 14 | url: @base_url <> "/dimensions" 15 | } -> 16 | %Tesla.Env{status: 200, body: Mux.Fixtures.monitoring_dimensions()} 17 | 18 | %{ 19 | method: :get, 20 | url: @base_url <> "/metrics" 21 | } -> 22 | %Tesla.Env{status: 200, body: Mux.Fixtures.monitoring_metrics()} 23 | 24 | %{ 25 | method: :get, 26 | url: @base_url <> "/metrics/playback-failure-percentage/breakdown", 27 | query: [ 28 | dimension: "country", 29 | timestamp: 1_547_853_000, 30 | filters: ["operating_system:windows"] 31 | ] 32 | } -> 33 | %Tesla.Env{status: 200, body: Mux.Fixtures.monitoring_breakdown()} 34 | 35 | %{ 36 | method: :get, 37 | url: @base_url <> "/metrics/video-startup-time/histogram-timeseries", 38 | query: [filters: ["operating_system:windows", "country:US"]] 39 | } -> 40 | %Tesla.Env{status: 200, body: Mux.Fixtures.monitoring_histogram_timeseries()} 41 | 42 | %{ 43 | method: :get, 44 | url: @base_url <> "/metrics/playback-failure-percentage/timeseries", 45 | query: [filters: ["operating_system:windows", "country:US"]] 46 | } -> 47 | %Tesla.Env{status: 200, body: Mux.Fixtures.monitoring_timeseries()} 48 | end) 49 | 50 | {:ok, %{client: client}} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/mux/video/tracks.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.Tracks do 2 | @moduledoc """ 3 | This module provides functions around managing tracks in Mux Video. Tracks are 4 | used for subtitles/captions. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-asset-track). 5 | """ 6 | alias Mux.{Base, Fixtures} 7 | 8 | @doc """ 9 | Create a new asset track. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-asset-track) 10 | 11 | Returns `{:ok, track, raw_env}`. 12 | 13 | ## Examples 14 | 15 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 16 | iex> {:ok, track, _env} = Mux.Video.Tracks.create(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{url: "https://example.com/myVideo_en.srt", type: "text", text_type: "subtitles", language_code: "en" }) 17 | iex> track 18 | #{inspect(Fixtures.track())} 19 | 20 | """ 21 | def create(client, asset_id, params) do 22 | Base.post(client, build_base_path(asset_id), params) 23 | end 24 | 25 | @doc """ 26 | Delete an asset track. [API Documentation](https://docs.mux.com/api-reference/video#operation/delete-asset-track) 27 | 28 | Returns `{:ok, "", raw_env}`. 29 | 30 | ## Examples 31 | 32 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 33 | iex> {status, "", _env} = Mux.Video.Tracks.delete(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 34 | iex> status 35 | :ok 36 | 37 | """ 38 | def delete(client, asset_id, track_id) do 39 | Base.delete(client, build_base_path(asset_id) <> "/" <> track_id) 40 | end 41 | 42 | defp build_base_path(asset_id), do: "/video/v1/assets/#{asset_id}/tracks" 43 | end 44 | -------------------------------------------------------------------------------- /test/mux/video/playback_restriction_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.PlaybackRestrictionsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | 5 | doctest Mux.Video.PlaybackRestrictions 6 | 7 | @base_url "https://api.mux.com/video/v1/playback-restrictions" 8 | 9 | setup do 10 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 11 | 12 | mock(fn 13 | %{method: :get, url: @base_url} -> 14 | %Tesla.Env{ 15 | status: 200, 16 | body: %{ 17 | "data" => [ 18 | Mux.Fixtures.playback_restriction(), 19 | Mux.Fixtures.playback_restriction() 20 | ] 21 | } 22 | } 23 | 24 | %{method: :get, url: @base_url <> "/uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3"} -> 25 | %Tesla.Env{ 26 | status: 200, 27 | body: %{ 28 | "data" => Mux.Fixtures.playback_restriction() 29 | } 30 | } 31 | 32 | %{method: :post, url: @base_url} -> 33 | %Tesla.Env{ 34 | status: 201, 35 | body: %{ 36 | "data" => Mux.Fixtures.playback_restriction() 37 | } 38 | } 39 | 40 | %{ 41 | method: :delete, 42 | url: 43 | @base_url <> 44 | "/uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3" 45 | } -> 46 | %Tesla.Env{status: 204, body: ""} 47 | 48 | %{ 49 | method: :put, 50 | url: @base_url <> "/uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3/referrer" 51 | } -> 52 | %Tesla.Env{ 53 | status: 200, 54 | body: %{ 55 | "data" => Mux.Fixtures.playback_restriction() 56 | } 57 | } 58 | end) 59 | 60 | {:ok, %{client: client}} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/mux/video/transcription_vocabularies_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.TranscriptionVocabulariesTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | 5 | doctest Mux.Video.TranscriptionVocabularies 6 | 7 | @base_url "https://api.mux.com/video/v1/transcription-vocabularies" 8 | 9 | setup do 10 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 11 | 12 | mock(fn 13 | %{method: :get, url: @base_url} -> 14 | %Tesla.Env{ 15 | status: 200, 16 | body: %{ 17 | "data" => [ 18 | Mux.Fixtures.transcription_vocabulary(), 19 | Mux.Fixtures.transcription_vocabulary(:update) 20 | ] 21 | } 22 | } 23 | 24 | %{method: :get, url: @base_url <> "/RjFsousKxDrwqqGtwuLIAzrmtCb016fTK"} -> 25 | %Tesla.Env{ 26 | status: 200, 27 | body: %{ 28 | "data" => Mux.Fixtures.transcription_vocabulary() 29 | } 30 | } 31 | 32 | %{method: :post, url: @base_url} -> 33 | %Tesla.Env{ 34 | status: 201, 35 | body: %{ 36 | "data" => Mux.Fixtures.transcription_vocabulary() 37 | } 38 | } 39 | 40 | %{ 41 | method: :delete, 42 | url: 43 | @base_url <> 44 | "/RjFsousKxDrwqqGtwuLIAzrmtCb016fTK" 45 | } -> 46 | %Tesla.Env{status: 204, body: ""} 47 | 48 | %{ 49 | method: :put, 50 | url: 51 | @base_url <> 52 | "/ANZLqMO4E01TQW01SyFJfrdZzvjMVuyYqE" 53 | } -> 54 | %Tesla.Env{status: 200, body: %{"data" => Mux.Fixtures.transcription_vocabulary(:update)}} 55 | end) 56 | 57 | {:ok, %{client: client}} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/mux.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux do 2 | @external_resource "README.md" 3 | @moduledoc @external_resource 4 | |> File.read!() 5 | |> String.split("") 6 | |> Enum.fetch!(1) 7 | 8 | @doc """ 9 | Same as `Mux.client/3`, but will attempt to pull your access token ID and secret from the 10 | application configuration. 11 | 12 | Returns `%Tesla.Client{}`. 13 | 14 | ## Examples 15 | 16 | iex> Application.put_env(:mux, :access_token_id, "my_token_id") 17 | iex> Application.put_env(:mux, :access_token_secret, "my_token_secret") 18 | iex> Mux.client() 19 | %Tesla.Client{ 20 | fun: nil, 21 | post: [], 22 | pre: [ 23 | {Tesla.Middleware.BaseUrl, :call, ["https://api.mux.com"]}, 24 | {Tesla.Middleware.BasicAuth, :call, 25 | [%{password: "my_token_secret", username: "my_token_id"}]} 26 | ] 27 | } 28 | 29 | """ 30 | def client(options \\ []), 31 | do: 32 | client( 33 | Application.get_env(:mux, :access_token_id), 34 | Application.get_env(:mux, :access_token_secret), 35 | options 36 | ) 37 | 38 | @doc """ 39 | Creates a new connection struct that takes an access token ID and 40 | secret, as well as params. 41 | 42 | Returns `%Tesla.Client{}`. 43 | 44 | ## Examples 45 | iex> Mux.client("my_token_id", "my_token_secret") 46 | %Tesla.Client{ 47 | fun: nil, 48 | post: [], 49 | pre: [ 50 | {Tesla.Middleware.BaseUrl, :call, ["https://api.mux.com"]}, 51 | {Tesla.Middleware.BasicAuth, :call, 52 | [%{password: "my_token_secret", username: "my_token_id"}]} 53 | ] 54 | } 55 | """ 56 | def client(id, secret, options \\ []), do: Mux.Base.new(id, secret, options) 57 | end 58 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.MixProject do 2 | use Mix.Project 3 | 4 | @github_url "https://github.com/muxinc/mux-elixir" 5 | 6 | @version "3.2.2" 7 | 8 | def project do 9 | [ 10 | app: :mux, 11 | version: @version, 12 | elixir: "~> 1.6", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | package: package(), 16 | 17 | # Docs 18 | name: "Mux", 19 | source_url: @github_url, 20 | homepage_url: "https://mux.com", 21 | docs: docs() 22 | ] 23 | end 24 | 25 | # Run "mix help compile.app" to learn about applications. 26 | def application do 27 | [ 28 | extra_applications: [:logger] 29 | ] 30 | end 31 | 32 | # Run "mix help deps" to learn about dependencies. 33 | defp deps do 34 | [ 35 | {:exvcr, "~> 0.10", only: :test}, 36 | {:tesla, "~> 1.0"}, 37 | {:jason, "~> 1.0"}, 38 | {:jose, "~> 1.9"}, 39 | {:ex_doc, "~> 0.20", only: :dev, runtime: false}, 40 | {:mix_test_watch, "~> 0.5", only: :dev, runtime: false} 41 | ] 42 | end 43 | 44 | defp docs do 45 | [ 46 | groups_for_modules: groups_for_modules(), 47 | logo: "logo.png", 48 | main: "Mux", 49 | nest_modules_by_prefix: [Mux.Data, Mux.Video], 50 | source_ref: "v#{@version}", 51 | source_url: "https://github.com/muxinc/mux-elixir" 52 | ] 53 | end 54 | 55 | defp groups_for_modules do 56 | [ 57 | Data: ~r/Mux.Data.*/, 58 | Video: ~r/Mux.Video.*/ 59 | ] 60 | end 61 | 62 | defp package do 63 | [ 64 | files: ["lib", "mix.exs", "README.md", "LICENSE*"], 65 | maintainers: ["Matthew McClure "], 66 | licenses: ["MIT"], 67 | links: %{"GitHub" => @github_url}, 68 | description: "Official Elixir package for interacting with the Mux APIs" 69 | ] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/mux/data/video_views.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.VideoViews do 2 | @moduledoc """ 3 | This module provides functions for interacting with individual video views. Note, the raw video views 4 | contain _quite_ a lot of data, so the `list` endpoint includes a filtered set of keys for each view. 5 | [API Documentation](https://docs.mux.com/api-reference/data#tag/video-views). 6 | """ 7 | alias Mux.{Base, Fixtures} 8 | 9 | @doc """ 10 | Returns a paginated list of video views that occurred within the specified 11 | timeframe. Results are ordered by `view_end`, according to what you provide for 12 | `order_direction`. 13 | 14 | Returns `{:ok, views, raw_env}`. 15 | 16 | ## Examples 17 | 18 | iex> client = Mux.client("my_token_id", "my_token_secret") 19 | iex> {:ok, views, _env} = Mux.Data.VideoViews.list(client) 20 | iex> views 21 | #{inspect(Fixtures.video_views()["data"])} 22 | 23 | iex> client = Mux.client("my_token_id", "my_token_secret") 24 | iex> {:ok, views, _env} = Mux.Data.VideoViews.list(client, filters: ["browser:Chrome"], order_direction: "desc", page: 2) 25 | iex> views 26 | #{inspect(Fixtures.video_views()["data"])} 27 | 28 | """ 29 | def list(client, params \\ []) do 30 | Base.get(client, build_base_path(), query: params) 31 | end 32 | 33 | @doc """ 34 | Returns the details for a single video view. 35 | 36 | Returns `{:ok, view, raw_env}`. 37 | 38 | ## Examples 39 | 40 | iex> client = Mux.client("my_token_id", "my_token_secret") 41 | iex> {:ok, view, _env} = Mux.Data.VideoViews.get(client, "k8n4aklUyrRDekILDWta1qSJqNFpYB7N50") 42 | iex> view["id"] === "k8n4aklUyrRDekILDWta1qSJqNFpYB7N50" 43 | true 44 | 45 | """ 46 | def get(client, id) do 47 | Base.get(client, build_base_path() <> "/#{id}") 48 | end 49 | 50 | defp build_base_path(), do: "/data/v1/video-views" 51 | end 52 | -------------------------------------------------------------------------------- /lib/mux/data/incidents.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Incidents do 2 | @moduledoc """ 3 | This module includes functions for retrieving available incidents and their values in our system. These 4 | endpoints, for example, are used to construct the incidents tables in the alerts UI. [API Documentation](https://docs.mux.com/api-reference/data#operation/list-incidents) 5 | """ 6 | alias Mux.{Base, Fixtures} 7 | 8 | @doc """ 9 | Lists all incidents. 10 | 11 | Returns `{:ok, incidents, raw_env}`. 12 | 13 | ## Examples 14 | 15 | iex> client = Mux.client("my_token_id", "my_token_secret") 16 | iex> {:ok, incidents, _env} = Mux.Data.Incidents.list(client, status: 'open', severity: 'alert') 17 | iex> incidents 18 | #{inspect(Fixtures.incidents()["data"])} 19 | 20 | """ 21 | def list(client, params \\ []) do 22 | Base.get(client, build_base_path(), query: params) 23 | end 24 | 25 | @doc """ 26 | Lists details for a single incident. 27 | 28 | Returns `{:ok, incident, raw_env}`. 29 | 30 | ## Examples 31 | 32 | iex> client = Mux.client("my_token_id", "my_token_secret") 33 | iex> {:ok, incident, _env} = Mux.Data.Incidents.get(client, "ABCD1234") 34 | iex> incident 35 | #{inspect(Fixtures.incident()["data"])} 36 | 37 | """ 38 | def get(client, id) do 39 | Base.get(client, build_base_path() <> "/#{id}") 40 | end 41 | 42 | @doc """ 43 | Lists all the incidents that seem related to a specific incident. 44 | 45 | Returns `{:ok, incidents, raw_env}`. 46 | 47 | ## Examples 48 | 49 | iex> client = Mux.client("my_token_id", "my_token_secret") 50 | iex> {:ok, incidents, _env} = Mux.Data.Incidents.related(client, "ABCD1234", measurement: "median") 51 | iex> incidents 52 | #{inspect(Fixtures.related_incidents()["data"])} 53 | 54 | """ 55 | def related(client, id, params \\ []) do 56 | Base.get(client, build_base_path() <> "/#{id}/related", query: params) 57 | end 58 | 59 | defp build_base_path(), do: "/data/v1/incidents" 60 | end 61 | -------------------------------------------------------------------------------- /lib/mux/webhooks/test_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Webhooks.TestUtils do 2 | @moduledoc """ 3 | This module provides a utility function for generating valid signatures. 4 | You are encouraged to use it for testing how your application behaves 5 | against malicious requests. 6 | """ 7 | 8 | @default_scheme "v1" 9 | 10 | @doc """ 11 | Generates a webhook signature. Pass in the raw request body and the 12 | webhook "secret". You may also pass in the desired signature scheme. 13 | Currently, the only valid signature scheme is `"v1"`, which is also the 14 | default one if not set explicitly. Any invalid signature scheme will fall 15 | back to the default. 16 | 17 | Note that this function is intended for testing against malformed or 18 | malicious webhook requests, so you should use a fake webhook "secret". 19 | 20 | Returns the relevant HTTP header value as a string. 21 | 22 | ## Examples 23 | 24 | iex> signature = Mux.Webhooks.TestUtils.generate_signature("payload", "SuperSecret123") 25 | "t=1591664030,v1=e43496b6aae982c4c2fd6f8e92935f1d90216f1f64d56024e72390acfb988272" 26 | 27 | iex> Mux.Webhooks.verify_header("payload", signature, "SuperSecret123") 28 | :ok 29 | """ 30 | def generate_signature(payload, secret, _scheme \\ @default_scheme) do 31 | # As long the `v1` will be the only signature scheme available, we can 32 | # simply ignore `_scheme`. 33 | timestamp = System.system_time(:second) 34 | signed_payload = "#{timestamp}.#{payload}" 35 | signature = compute_signature(signed_payload, secret) 36 | 37 | "t=#{timestamp},#{@default_scheme}=#{signature}" 38 | end 39 | 40 | def compute_signature(payload, secret) do 41 | hmac(:sha256, secret, payload) 42 | |> Base.encode16(case: :lower) 43 | end 44 | 45 | if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do 46 | defp hmac(digest, key, payload), do: :crypto.mac(:hmac, digest, key, payload) 47 | else 48 | defp hmac(digest, key, payload), do: :crypto.hmac(digest, key, payload) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/mux/video/signing_keys.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.SigningKeys do 2 | @moduledoc """ 3 | This module provides functions for managing signing keys in Mux Video. [API Documentation](https://docs.mux.com/guides/video/secure-video-playback) 4 | """ 5 | 6 | alias Mux.{Base, Fixtures} 7 | 8 | @path "/video/v1/signing-keys/" 9 | 10 | @doc """ 11 | Create a new signing key. 12 | 13 | Returns `{:ok, signing_key, %Tesla.Client{}}`. 14 | 15 | ## Examples 16 | 17 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 18 | iex> Mux.Video.SigningKeys.create(client) 19 | {:ok, #{inspect(Fixtures.signing_key(:create))}, #{inspect(Fixtures.tesla_env({:signing_key, [:create]}))}} 20 | 21 | """ 22 | def create(client) do 23 | Base.post(client, @path, %{}) 24 | end 25 | 26 | @doc """ 27 | List signing keys. 28 | 29 | Returns a tuple such as `{:ok, signing_keys, %Telsa.Env{}}` 30 | 31 | ## Examples 32 | 33 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 34 | iex> {:ok, signing_keys, _env} = Mux.Video.SigningKeys.list(client) 35 | iex> signing_keys 36 | #{inspect([Fixtures.signing_key(), Fixtures.signing_key()])} 37 | 38 | """ 39 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 40 | 41 | @doc """ 42 | Retrieve a signing key by ID. 43 | 44 | Returns a tuple such as `{:ok, signing_key, %Telsa.Env{}}` 45 | 46 | ## Examples 47 | 48 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 49 | iex> {:ok, signing_key, _env} = Mux.Video.SigningKeys.get(client, "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C") 50 | iex> signing_key 51 | #{inspect(Fixtures.signing_key())} 52 | 53 | """ 54 | def get(client, key_id, options \\ []) do 55 | Base.get(client, @path <> key_id, query: options) 56 | end 57 | 58 | @doc """ 59 | Delete a signing key. 60 | 61 | Returns a tuple such as `{:ok, "", %Telsa.Env{}}` 62 | 63 | ## Examples 64 | 65 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 66 | iex> {status, "", _env} = Mux.Video.SigningKeys.delete(client, "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C") 67 | iex> status 68 | :ok 69 | 70 | """ 71 | def delete(client, key_id, params \\ []) do 72 | Base.delete(client, @path <> key_id, query: params) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/mux/base.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Base do 2 | @moduledoc false 3 | 4 | @type result :: {:ok, any, Tesla.Env.t()} | {:error, String.t(), any} 5 | 6 | @doc """ 7 | Wrapper for Tesla.get/3 that returns a simplified response 8 | """ 9 | @spec get(Tesla.Env.client(), String.t(), any) :: result() 10 | def get(client, path, params \\ []) do 11 | client 12 | |> Mux.Tesla.get(path, params) 13 | |> simplify_response() 14 | end 15 | 16 | @doc """ 17 | Wrapper for Tesla.post/3 that returns a simplified response 18 | """ 19 | @spec post(Tesla.Env.client(), String.t(), any) :: result() 20 | def post(client, path, params) do 21 | client 22 | |> Mux.Tesla.post(path, params) 23 | |> simplify_response() 24 | end 25 | 26 | @doc """ 27 | Wrapper for Tesla.put/3 that returns a simplified response 28 | """ 29 | @spec put(Tesla.Env.client(), String.t(), any) :: result() 30 | def put(client, path, params) do 31 | client 32 | |> Mux.Tesla.put(path, params) 33 | |> simplify_response() 34 | end 35 | 36 | @doc """ 37 | Wrapper for Tesla.patch/3 that returns a simplified response 38 | """ 39 | @spec patch(Tesla.Env.client(), String.t(), any) :: result() 40 | def patch(client, path, params) do 41 | client 42 | |> Mux.Tesla.patch(path, params) 43 | |> simplify_response() 44 | end 45 | 46 | @doc """ 47 | Wrapper for Tesla.delete/2 that returns a simplified response 48 | """ 49 | @spec delete(Tesla.Env.client(), String.t(), any) :: result() 50 | def delete(client, path, params \\ []) do 51 | client 52 | |> Mux.Tesla.delete(path, params) 53 | |> simplify_response() 54 | end 55 | 56 | defdelegate new(token_id, token_secret, opts \\ []), to: Mux.Tesla 57 | 58 | @spec simplify_response(Tesla.Env.result()) :: result() 59 | defp simplify_response({:ok, %{status: status} = resp}) when status >= 200 and status < 300 do 60 | {:ok, get_response_contents(resp.body), resp} 61 | end 62 | 63 | defp simplify_response({:ok, %{body: %{"error" => %{"type" => type, "messages" => messages}}}}) do 64 | {:error, type, messages} 65 | end 66 | 67 | defp simplify_response(error) do 68 | raise Mux.Exception, error 69 | end 70 | 71 | defp get_response_contents(body) when is_map(body), do: body["data"] 72 | defp get_response_contents(body), do: body 73 | end 74 | -------------------------------------------------------------------------------- /lib/mux/video/uploads.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.Uploads do 2 | @moduledoc """ 3 | This module provides functions for managing direct uploads in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/direct-uploads) 4 | """ 5 | 6 | alias Mux.{Base, Fixtures} 7 | 8 | @path "/video/v1/uploads/" 9 | 10 | @doc """ 11 | Create a new direct upload. 12 | 13 | Returns `{:ok, upload, %Tesla.Client{}}`. 14 | 15 | ## Examples 16 | 17 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 18 | iex> params = %{"new_asset_settings" => %{"playback_policies" => ["public"]}, "cors_origin" => "http://localhost:8080"} 19 | iex> Mux.Video.Uploads.create(client, params) 20 | {:ok, #{inspect(Fixtures.upload(:create))}, #{inspect(Fixtures.tesla_env({:upload, [:create]}))}} 21 | 22 | """ 23 | def create(client, params) do 24 | Base.post(client, @path, params) 25 | end 26 | 27 | @doc """ 28 | List direct uploads. 29 | 30 | Returns a tuple such as `{:ok, uploads, %Telsa.Env{}}` 31 | 32 | ## Examples 33 | 34 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 35 | iex> {:ok, uploads, _env} = Mux.Video.Uploads.list(client) 36 | iex> uploads 37 | #{inspect([Fixtures.upload(), Fixtures.upload()])} 38 | 39 | """ 40 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 41 | 42 | @doc """ 43 | Retrieve a direct upload by ID. 44 | 45 | Returns a tuple such as `{:ok, upload, %Telsa.Env{}}` 46 | 47 | ## Examples 48 | 49 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 50 | iex> {:ok, upload, _env} = Mux.Video.Uploads.get(client, "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM") 51 | iex> upload 52 | #{inspect(Fixtures.upload())} 53 | 54 | """ 55 | def get(client, key_id, options \\ []) do 56 | Base.get(client, @path <> key_id, query: options) 57 | end 58 | 59 | @doc """ 60 | Cancel a direct upload. 61 | 62 | Returns a tuple such as `{:ok, %Telsa.Env{}}` 63 | 64 | ## Examples 65 | 66 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 67 | iex> {:ok, direct_upload, _env} = Mux.Video.Uploads.cancel(client, "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM") 68 | iex> direct_upload 69 | #{inspect(Fixtures.upload(:cancel))} 70 | 71 | """ 72 | def cancel(client, key_id) do 73 | Base.put(client, @path <> key_id <> "/cancel", %{}) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/mux/video/spaces_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.SpacesTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | 5 | doctest Mux.Video.Spaces 6 | 7 | @base_url "https://api.mux.com/video/v1/spaces" 8 | 9 | setup do 10 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 11 | 12 | mock(fn 13 | %{method: :get, url: @base_url} -> 14 | %Tesla.Env{ 15 | status: 200, 16 | body: %{ 17 | "data" => [ 18 | Mux.Fixtures.space(), 19 | Mux.Fixtures.space() 20 | ] 21 | } 22 | } 23 | 24 | %{method: :get, url: @base_url <> "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk"} -> 25 | %Tesla.Env{ 26 | status: 200, 27 | body: %{ 28 | "data" => Mux.Fixtures.space() 29 | } 30 | } 31 | 32 | %{method: :post, url: @base_url} -> 33 | %Tesla.Env{ 34 | status: 201, 35 | body: %{ 36 | "data" => Mux.Fixtures.space() 37 | } 38 | } 39 | 40 | %{ 41 | method: :delete, 42 | url: 43 | @base_url <> 44 | "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk" 45 | } -> 46 | %Tesla.Env{status: 204, body: ""} 47 | 48 | %{ 49 | method: :post, 50 | url: @base_url <> "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk/broadcasts" 51 | } -> 52 | %Tesla.Env{status: 201, body: %{"data" => Mux.Fixtures.broadcast()}} 53 | 54 | %{ 55 | method: :get, 56 | url: 57 | @base_url <> 58 | "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk/broadcasts/fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ" 59 | } -> 60 | %Tesla.Env{status: 200, body: %{"data" => Mux.Fixtures.broadcast()}} 61 | 62 | %{ 63 | method: :delete, 64 | url: 65 | @base_url <> 66 | "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk/broadcasts/fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ" 67 | } -> 68 | %Tesla.Env{status: 204, body: ""} 69 | 70 | %{ 71 | method: :post, 72 | url: 73 | @base_url <> 74 | "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk/broadcasts/fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ/start" 75 | } -> 76 | %Tesla.Env{status: 200, body: %{"data" => %{}}} 77 | 78 | %{ 79 | method: :post, 80 | url: 81 | @base_url <> 82 | "/xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk/broadcasts/fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ/stop" 83 | } -> 84 | %Tesla.Env{status: 200, body: %{"data" => %{}}} 85 | end) 86 | 87 | {:ok, %{client: client}} 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/mux/video/playback_restrictions.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.PlaybackRestrictions do 2 | @moduledoc """ 3 | This module provides functions for managing Playback Restrictions in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/playback-restrictions) 4 | """ 5 | alias Mux.{Base, Fixtures} 6 | 7 | @path "/video/v1/playback-restrictions" 8 | 9 | @doc """ 10 | Create a new playback restriction. 11 | 12 | Returns `{:ok, playback_restriction, %Tesla.Client{}}`. 13 | 14 | ## Examples 15 | 16 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 17 | iex> {:ok, playback_restriction, _env} = Mux.Video.PlaybackRestrictions.create(client, %{referrer: %{allowed_domains: ["*.example.com"], allow_no_referrer: true}}) 18 | iex> playback_restriction 19 | #{inspect(Fixtures.playback_restriction())} 20 | 21 | """ 22 | def create(client, params) do 23 | Base.post(client, @path, params) 24 | end 25 | 26 | @doc """ 27 | List playback restrictions. 28 | 29 | Returns a tuple such as `{:ok, playback_restrictions, %Telsa.Env{}}` 30 | 31 | ## Examples 32 | 33 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 34 | iex> {:ok, playback_restrictions, _env} = Mux.Video.PlaybackRestrictions.list(client) 35 | iex> playback_restrictions 36 | #{inspect([Fixtures.playback_restriction(), Fixtures.playback_restriction()])} 37 | 38 | """ 39 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 40 | 41 | @doc """ 42 | Retrieve a playback restriction by ID. 43 | 44 | Returns a tuple such as `{:ok, playback_restriction, %Telsa.Env{}}` 45 | 46 | ## Examples 47 | 48 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 49 | iex> {:ok, playback_restriction, _env} = Mux.Video.PlaybackRestrictions.get(client, "uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3") 50 | iex> playback_restriction 51 | #{inspect(Fixtures.playback_restriction())} 52 | 53 | """ 54 | def get(client, playback_restriction_id, options \\ []) do 55 | Base.get(client, "#{@path}/#{playback_restriction_id}", query: options) 56 | end 57 | 58 | @doc """ 59 | Delete a playback restriction. 60 | 61 | Returns a tuple such as `{:ok, "", %Telsa.Env{}}` 62 | 63 | ## Examples 64 | 65 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 66 | iex> {status, "", _env} = Mux.Video.PlaybackRestrictions.delete(client, "uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3") 67 | iex> status 68 | :ok 69 | 70 | """ 71 | def delete(client, playback_restriction_id, params \\ []) do 72 | Base.delete(client, "#{@path}/#{playback_restriction_id}", query: params) 73 | end 74 | 75 | @doc """ 76 | Updates the referrer domain restriction for a playback restriction 77 | 78 | Returns a tuple such as `{:ok, playback_restriction, %Tesla.Env{}}` 79 | 80 | ## Examples 81 | 82 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 83 | iex> {:ok, playback_restriction, _env} = Mux.Video.PlaybackRestrictions.update_referrer_domain_restriction(client, "uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3", %{allowed_domains: ["*.example.com"], allow_no_referrer: true}) 84 | iex> playback_restriction 85 | #{inspect(Fixtures.playback_restriction())} 86 | 87 | """ 88 | def update_referrer_domain_restriction(client, playback_restriction_id, params) do 89 | Base.put(client, "#{@path}/#{playback_restriction_id}/referrer", params) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/mux/webhooks.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Webhooks do 2 | @moduledoc """ 3 | This module provides a function for verifying webhook signatures 4 | """ 5 | @default_tolerance 300 6 | @expected_scheme "v1" 7 | 8 | @doc """ 9 | Verifies a webhook signature. Pass in the raw request body, the 10 | signature header that came with the webhook request ('Mux-Signature') 11 | and the webhook "secret" from your webhooks dashboard. Note that the 12 | webhook secret is different than your API secret. 13 | 14 | Returns `:ok`. Or a tuple with `{:error, "message"}` 15 | 16 | ## Examples 17 | 18 | iex> Mux.Webhooks.verify_header(raw_request_body, signature_header, secret) 19 | :ok 20 | """ 21 | def verify_header(payload, signature_header, secret, tolerance \\ @default_tolerance) do 22 | case get_timestamp_and_signatures(signature_header, @expected_scheme) do 23 | {nil, _} -> 24 | {:error, "Unable to extract timestamp and signatures from header"} 25 | 26 | {_, []} -> 27 | {:error, "No signatures found with expected scheme #{@expected_scheme}"} 28 | 29 | {timestamp, signatures} -> 30 | with {:ok, timestamp} <- check_timestamp(timestamp, tolerance), 31 | {:ok, _signatures} <- check_signatures(signatures, timestamp, payload, secret) do 32 | :ok 33 | else 34 | {:error, error} -> {:error, error} 35 | end 36 | end 37 | end 38 | 39 | defp get_timestamp_and_signatures(signature_header, scheme) do 40 | signature_header 41 | |> String.split(",") 42 | |> Enum.map(&String.split(&1, "=")) 43 | |> Enum.reduce({nil, []}, fn 44 | ["t", timestamp], {nil, signatures} -> 45 | {to_integer(timestamp), signatures} 46 | 47 | [^scheme, signature], {timestamp, signatures} -> 48 | {timestamp, [signature | signatures]} 49 | 50 | _, acc -> 51 | acc 52 | end) 53 | end 54 | 55 | defp to_integer(timestamp) do 56 | case Integer.parse(timestamp) do 57 | {timestamp, _} -> 58 | timestamp 59 | 60 | :error -> 61 | nil 62 | end 63 | end 64 | 65 | defp check_timestamp(timestamp, tolerance) do 66 | now = System.system_time(:second) 67 | 68 | if timestamp < now - tolerance do 69 | {:error, "Timestamp outside the tolerance zone"} 70 | else 71 | {:ok, timestamp} 72 | end 73 | end 74 | 75 | defp check_signatures(signatures, timestamp, payload, secret) do 76 | signed_payload = "#{timestamp}.#{payload}" 77 | expected_signature = compute_signature(signed_payload, secret) 78 | 79 | if Enum.any?(signatures, &secure_equals?(&1, expected_signature)) do 80 | {:ok, signatures} 81 | else 82 | {:error, "No signatures found matching the expected signature for payload"} 83 | end 84 | end 85 | 86 | defp secure_equals?(input, expected) when byte_size(input) == byte_size(expected) do 87 | input = String.to_charlist(input) 88 | expected = String.to_charlist(expected) 89 | secure_compare(input, expected) 90 | end 91 | 92 | defp secure_equals?(_, _), do: false 93 | 94 | defp secure_compare(acc \\ 0, input, expected) 95 | defp secure_compare(acc, [], []), do: acc == 0 96 | 97 | defp secure_compare(acc, [input_codepoint | input], [expected_codepoint | expected]) do 98 | import Bitwise 99 | 100 | acc 101 | |> bor(bxor(input_codepoint, expected_codepoint)) 102 | |> secure_compare(input, expected) 103 | end 104 | 105 | def compute_signature(payload, secret) do 106 | hmac(:sha256, secret, payload) 107 | |> Base.encode16(case: :lower) 108 | end 109 | 110 | if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do 111 | defp hmac(digest, key, payload), do: :crypto.mac(:hmac, digest, key, payload) 112 | else 113 | defp hmac(digest, key, payload), do: :crypto.hmac(digest, key, payload) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/mux/video/transcription_vocabularies.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.TranscriptionVocabularies do 2 | @moduledoc """ 3 | This module provides functions around managing transcription vocabularies in Mux Video. Transcription vocabularies 4 | allow you to provide collections of phrases like proper nouns, technical jargon, and uncommon words as part of 5 | captioning workflows. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-transcription-vocabulary). 6 | """ 7 | alias Mux.{Base, Fixtures} 8 | 9 | @path "/video/v1/transcription-vocabularies" 10 | 11 | @doc """ 12 | Create a new transcription vocabulary. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-transcription-vocabulary) 13 | 14 | Returns `{:ok, transcription_vocabulary, raw_env}`. 15 | 16 | ## Examples 17 | 18 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 19 | iex> {:ok, transcription_vocabulary, _env} = Mux.Video.TranscriptionVocabularies.create(client, %{name: "API Vocabulary", phrases: ["Mux", "Live Stream", "Playback ID"]}) 20 | iex> transcription_vocabulary 21 | #{inspect(Fixtures.transcription_vocabulary())} 22 | 23 | """ 24 | def create(client, params) do 25 | Base.post(client, @path, params) 26 | end 27 | 28 | @doc """ 29 | List transcription vocabularies. [API Documentation](https://docs.mux.com/api-reference/video#operation/list-transcription-vocabularies) 30 | 31 | Returns a tuple such as `{:ok, transcription_vocabularies, %Telsa.Env{}}` 32 | 33 | ## Examples 34 | 35 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 36 | iex> {:ok, transcription_vocabularies, _env} = Mux.Video.TranscriptionVocabularies.list(client) 37 | iex> transcription_vocabularies 38 | #{inspect([Fixtures.transcription_vocabulary(), Fixtures.transcription_vocabulary(:update)])} 39 | 40 | """ 41 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 42 | 43 | @doc """ 44 | Retrieve a transcription vocabulary by ID. [API Documentation](https://docs.mux.com/api-reference/video#operation/get-transcription-vocabulary) 45 | 46 | Returns a tuple such as `{:ok, transcription_vocabulary, %Telsa.Env{}}` 47 | 48 | ## Examples 49 | 50 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 51 | iex> {:ok, transcription_vocabulary, _env} = Mux.Video.TranscriptionVocabularies.get(client, "RjFsousKxDrwqqGtwuLIAzrmtCb016fTK") 52 | iex> transcription_vocabulary 53 | #{inspect(Fixtures.transcription_vocabulary())} 54 | 55 | """ 56 | def get(client, transcription_vocabulary_id, options \\ []) do 57 | Base.get(client, "#{@path}/#{transcription_vocabulary_id}", query: options) 58 | end 59 | 60 | @doc """ 61 | Delete a transcription vocabulary. [API Documentation](https://docs.mux.com/api-reference/video#operation/delete-transcription-vocabulary) 62 | 63 | Returns `{:ok, "", raw_env}`. 64 | 65 | ## Examples 66 | 67 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 68 | iex> {status, "", _env} = Mux.Video.TranscriptionVocabularies.delete(client, "RjFsousKxDrwqqGtwuLIAzrmtCb016fTK") 69 | iex> status 70 | :ok 71 | 72 | """ 73 | def delete(client, transcription_vocabulary_id) do 74 | Base.delete(client, "#{@path}/#{transcription_vocabulary_id}") 75 | end 76 | 77 | @doc """ 78 | Updates a transcription vocabulary 79 | 80 | Returns a tuple such as `{:ok, transcription_vocabulary, %Tesla.Env{}} 81 | 82 | ## Examples 83 | 84 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 85 | iex> {:ok, transcription_vocabulary, _env} = Mux.Video.TranscriptionVocabularies.update(client, "ANZLqMO4E01TQW01SyFJfrdZzvjMVuyYqE", %{name: "New API Vocabulary", phrases: ["Mux", "Live Stream", "Playback ID", "New phrase"]}) 86 | iex> transcription_vocabulary 87 | #{inspect(Fixtures.transcription_vocabulary(:update))} 88 | """ 89 | def update(client, transcription_vocabulary_id, params) do 90 | Base.put(client, "#{@path}/#{transcription_vocabulary_id}", params) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/mux/data/monioring.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Monitoring do 2 | @moduledoc """ 3 | This module provides functions that interact with the `monitoring` endpoints 4 | 5 | Note, these API documentation links may break periodically as we update documentation titles. 6 | - [Dimensions](https://docs.mux.com/api-reference/data#operation/list-monitoring-dimensions) 7 | - [Metrics](https://docs.mux.com/api-reference/data#operation/list-monitoring-metrics) 8 | - [Breakdown](https://docs.mux.com/api-reference/data#operation/get-monitoring-breakdown) 9 | - [HistogramTimeseries](https://docs.mux.com/api-reference/data#operation/get-monitoring-histogram-timeseries) 10 | - [Timeseries](https://docs.mux.com/api-reference/data#operation/get-monitoring-timeseries) 11 | """ 12 | alias Mux.{Base, Fixtures} 13 | 14 | @doc """ 15 | List of available real-time dimensions 16 | 17 | Returns `{:ok, dimensions, raw_env}`. 18 | 19 | ## Examples 20 | 21 | iex> client = Mux.client("my_token_id", "my_token_secret") 22 | iex> {:ok, dimensions, _env} = Mux.Data.Monitoring.dimensions(client) 23 | iex> dimensions 24 | #{inspect(Fixtures.monitoring_dimensions()["data"])} 25 | 26 | """ 27 | def dimensions(client) do 28 | Base.get(client, build_base_path() <> "/dimensions") 29 | end 30 | 31 | @doc """ 32 | List of available real-time metrics 33 | 34 | Returns `{:ok, metrics, raw_env}`. 35 | 36 | ## Examples 37 | 38 | iex> client = Mux.client("my_token_id", "my_token_secret") 39 | iex> {:ok, metrics, _env} = Mux.Data.Monitoring.metrics(client) 40 | iex> metrics 41 | #{inspect(Fixtures.monitoring_metrics()["data"])} 42 | 43 | """ 44 | def metrics(client) do 45 | Base.get(client, build_base_path() <> "/metrics") 46 | end 47 | 48 | @doc """ 49 | Get breakdown information for a specific dimension and metric along with the number of concurrent viewers and negative impact score. 50 | 51 | Returns `{:ok, breakdown, raw_env}`. 52 | 53 | ## Examples 54 | 55 | iex> client = Mux.client("my_token_id", "my_token_secret") 56 | iex> {:ok, breakdown, _env} = Mux.Data.Monitoring.breakdown(client, "playback-failure-percentage", dimension: "country", timestamp: 1_547_853_000, filters: ["operating_system:windows"]) 57 | iex> breakdown 58 | #{inspect(Fixtures.monitoring_breakdown()["data"])} 59 | 60 | """ 61 | def breakdown(client, metric, params \\ []) do 62 | Base.get(client, build_base_path(metric) <> "/breakdown", query: params) 63 | end 64 | 65 | @doc """ 66 | List histogram timeseries information for a specific metric 67 | 68 | Returns `{:ok, histogram_timeseries, raw_env}`. 69 | 70 | ## Examples 71 | 72 | iex> client = Mux.client("my_token_id", "my_token_secret") 73 | iex> {:ok, histogram_timeseries, _env} = Mux.Data.Monitoring.histogram_timeseries(client, "video-startup-time", filters: ["operating_system:windows", "country:US"]) 74 | iex> histogram_timeseries 75 | #{inspect(Fixtures.monitoring_histogram_timeseries()["data"])} 76 | 77 | """ 78 | def histogram_timeseries(client, metric, params \\ []) do 79 | Base.get(client, build_base_path(metric) <> "/histogram-timeseries", query: params) 80 | end 81 | 82 | @doc """ 83 | List timeseries information for a specific metric along with the number of concurrent viewers. 84 | 85 | Returns `{:ok, timeseries, raw_env}`. 86 | 87 | ## Examples 88 | 89 | iex> client = Mux.client("my_token_id", "my_token_secret") 90 | iex> {:ok, timeseries, _env} = Mux.Data.Monitoring.timeseries(client, "playback-failure-percentage", filters: ["operating_system:windows", "country:US"]) 91 | iex> timeseries 92 | #{inspect(Fixtures.monitoring_timeseries()["data"])} 93 | 94 | """ 95 | def timeseries(client, metric, params \\ []) do 96 | Base.get(client, build_base_path(metric) <> "/timeseries", query: params) 97 | end 98 | 99 | defp build_base_path(), do: "/data/v1/monitoring" 100 | defp build_base_path(metric), do: build_base_path() <> "/metrics/#{metric}" 101 | end 102 | -------------------------------------------------------------------------------- /test/mux/token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.TokenTest do 2 | use ExUnit.Case 3 | 4 | @token_id "01XNj9qIpoW3eU1sED8EqrFRy01J3VTZ01x" 5 | @token_secret "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNGdoaERZN25QTzFhMmxibHN0T3JTcmt3VUdkdzVaT0tjR0h5U2NkWXpoc1FudHNnCkw4VWxHcURScVhuZFZ2Ky9rMEI2MnZxbzJGT1gvYkdVK2srT2lRang4UCswNVJKOWEyL1lpSzJjQm56MG15WEEKOS9CajRVNis1d3dCUk15Z2xCd2VaeWt1ZVNsZ1dYYnZRRm50bE1LcXptYU5XcnJvSDNpVEJZUU9xeFF5dHdkZApaSXlVeTYwNDJUMXJaQm5WOVN3RzA0UEhjaVJ3TjJ3Ly83YVhMY2FGRnJTOUVxZ29SNGszcjllblN1YWM3STRvCmV5NVJwMmFaVXlzK3U1VnMwUzluMzIzWVViMWZtRU1kZzEyWU1yMHIyL2Q5ZjMrdVZXQVVUTkQ1MitSREsvZ00KOEt6dW5FZ2w5eWFacit6VlFpa0RhOFoyOE9yOUxrN0xNNk5TNlFJREFRQUJBb0lCQURPbXRvYmlvUFRMU0hlYwpZK0Q1ZmFzVnBuUzVMcE5IbzlzS2h0TlZPblhldVcyVHBVZEZSYlZRQ3BrdnYrU2hqS1dabG5senppR2crSnFBCmVncTVJMWt0TWh4Z1VuWUdRNkxKYkRIUGVsZ0JOZVErUEZwc0ZHYm9GN2UwaHBXeUxQK3JiVWNsb2ZrTiszWjIKTnpYOVZzMG5ydUI3anRHczVGNU1yMHdUWVVhMmFNSHU2dnNNZFNYY00rZTVZS3FCQ3Z3dS9MZ0ZvVFE1YXh0RQo0ZzJYbU91YUQrSFBPU0o1T0Mwc3grK1RkbXBNL1lMK3RjVjZZNVBWbUFMUURMcmFJdU9UM01lcUlicElvRlZQCmcxeldSOTBqb3BKWVpLSVFoNEY4WE4xODR1bmZ6bjVJbW1JdktOWE9QSU1YMk1oekw2YU5rSXZ5YmdpWUIvVGwKMm5vdVNBRUNnWUVBNHlzZmVZL0h3Rkl4Vy9MZ2R3UHJubDR4aGkwRkFWeE1QK1k0alhYUHU1bklXS3dvUk1xbgplTEd6UnhESnlhQmN5NXgzVjFhTmYwblV1YzJqRUM2Yi9XUUo2VWJMUzJvK2J1NmdJR1NsbmxLWS9uSlJGTnpwClE0d0F1c1J3M2dTVi9FNW12QVJiYk1vQ3Rha3FxdXhhWlMzODQ1MEFaQkxqcm1kZDU1VlFPUVVDZ1lFQS9yaGIKRm9oVmdxZzY0QjFpU0FCc3dYdTdLNzZHQUN4ZEg0MmxoVjQvNktiUVRVNnFwRWxVZURwbWlVeUkxeVBxZ1MwbApLYS9XMGg5VDI5S0Z1bVlFOWdlUG02b0ZaZjIvQ0RnaWtTWHdQTk5kRWp2SmtrMXdnb3dLbDNNbUhuWGtTYUlZCmpYVG9Td2tmN2RJREhVRXQ3RnRVd1VsQXROM3N4TFcvWlVKSEI1VUNnWUFMOXkyRlBhbUwyOGgxeTJrL1c1bUIKa2Z5UjBMVSt5Um5MRTlsT3VqSGk4OHExd1B1dUErNm1VTlhjbkduRWtRblNQNytaZmhtZDVzbXByOGN6QndGNAphMWlLVFF4UVFKeGhRM2h6dkZsczZYVGRraS9ySldlMEF4L1d0cG9yVjVwKzI3SlZuUFVqMmRBaXVYSmg1bWtzCmd5dWE0WjR2cHo4TzVLcnhrOC9SOFFLQmdRQ1BJbE4xTHZrMktZaWtCWDhEek5GUVRGSWFPNzZhL0ZMNzl5R3EKOXhKY2p0aUFpSk1WTEd1OS83czhyZmc3Uk9CeTVFWjh6V1dldjZIazVjRGx4SXhISUdxUFk1UVRBdXJGR0o0OApDQ0NlWFh0d1VvNXJtdjU5TFdxS1BsZU9TRnNYRVhKUWt3QXhvaGdDRU1CVlFSb29OZzVEYXdGa1lVeTZJUk5ECk9HSW5uUUtCZ1FDeWo1UVU3YjcycktVaWlhTisxaExrVEJFQUFab01jRjY4Ukp5Mlh5VXo5bVJQK1hLMkthbHoKbVdXNFh2RHZoVlB1T25tNjJ1RkxURCsybG1mNjdwUlY3bHlSVWlycVFabzNUVmsweE8yS3JJV21uRm1EZzBSRQp6YnB6MVJsZy9ZVjhQU3E4alE3ZUFsRUs0RHRSdlVoQW5RRU52NlVyVjltRnRjR1k0ZFhSUmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=" 6 | 7 | describe "sign/2" do 8 | test "returns a valid token to use with a signed playback ID" do 9 | token = Mux.Token.sign("abcd1234", token_id: @token_id, token_secret: @token_secret) 10 | 11 | assert {true, _, _} = Mux.Token.verify(token, token_secret: @token_secret) 12 | end 13 | 14 | test "converts types to the appropriate `aud` type" do 15 | token = 16 | Mux.Token.sign("abcd1234", token_id: @token_id, token_secret: @token_secret, type: :gif) 17 | 18 | {valid, params, _} = Mux.Token.verify(token, token_secret: @token_secret) 19 | 20 | assert valid 21 | 22 | decoded_params = params |> Jason.decode!() 23 | assert decoded_params["aud"] === "g" 24 | end 25 | 26 | test "passes in query params" do 27 | token = 28 | Mux.Token.sign( 29 | "abcd1234", 30 | token_id: @token_id, 31 | token_secret: @token_secret, 32 | type: :thumbnail, 33 | params: %{height: 100} 34 | ) 35 | 36 | {valid, params, _} = Mux.Token.verify(token, token_secret: @token_secret) 37 | 38 | assert valid 39 | 40 | decoded_params = params |> Jason.decode!() 41 | assert decoded_params["aud"] === "t" 42 | assert decoded_params["height"] === 100 43 | end 44 | 45 | test "expiration value is added to the current timestamp" do 46 | expiration = 60 * 60 * 3 47 | 48 | token = 49 | Mux.Token.sign( 50 | "abcd1234", 51 | token_id: @token_id, 52 | token_secret: @token_secret, 53 | expiration: expiration 54 | ) 55 | 56 | {valid, params, _} = Mux.Token.verify(token, token_secret: @token_secret) 57 | 58 | assert valid 59 | 60 | %{"exp" => decoded_exp} = params |> Jason.decode!() 61 | 62 | # Just in case the test ran right on a second boundary, we just want to make sure these two timestamps 63 | # are within one second of each other. 64 | total_expiration_time = (DateTime.utc_now() |> DateTime.to_unix()) + expiration 65 | assert decoded_exp >= total_expiration_time - 1 && decoded_exp <= total_expiration_time + 1 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/mux/data/metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.Metrics do 2 | @moduledoc """ 3 | This module provides functions that interact with the `metrics` endpoints, which includes a bulk 4 | of the data product's statistical data. 5 | 6 | Note, these API documentation links may break periodically as we update documentation titles. 7 | - [Breakdowns](https://docs.mux.com/api-reference/data#operation/list-breakdown-values) 8 | - [Comparison](https://docs.mux.com/api-reference/data#operation/list-all-metric-values) 9 | - [Insights](https://docs.mux.com/api-reference/data#operation/list-insights) 10 | - [Overall](https://docs.mux.com/api-reference/data#operation/get-overall-values) 11 | - [Timeseries](https://docs.mux.com/api-reference/data#operation/get-metric-timeseries-data) 12 | """ 13 | alias Mux.{Base, Fixtures} 14 | 15 | @doc """ 16 | List the breakdown values for a specific metric. 17 | 18 | Returns `{:ok, breakdowns, raw_env}`. 19 | 20 | ## Examples 21 | 22 | iex> client = Mux.client("my_token_id", "my_token_secret") 23 | iex> {:ok, breakdowns, _env} = Mux.Data.Metrics.breakdown(client, "video_startup_time", "browser") 24 | iex> breakdowns 25 | #{inspect(Fixtures.breakdown()["data"])} 26 | 27 | iex> client = Mux.client("my_token_id", "my_token_secret") 28 | iex> {:ok, breakdowns, _env} = Mux.Data.Metrics.breakdown(client, "video_startup_time", "browser", measurement: "median", timeframe: ["6:hours"]) 29 | iex> breakdowns 30 | #{inspect(Fixtures.breakdown()["data"])} 31 | 32 | 33 | """ 34 | def breakdown(client, metric, group_by, params \\ []) do 35 | params = Keyword.merge([group_by: group_by], params) 36 | Base.get(client, build_base_path(metric) <> "/breakdown", query: params) 37 | end 38 | 39 | @doc """ 40 | List all of the values across every breakdown for a specific breakdown value. 41 | 42 | Returns `{:ok, comparisons, raw_env}`. 43 | 44 | ## Examples 45 | 46 | iex> client = Mux.client("my_token_id", "my_token_secret") 47 | iex> {:ok, comparison, _env} = Mux.Data.Metrics.comparison(client, "browser", "Safari") 48 | iex> comparison 49 | #{inspect(Fixtures.comparison()["data"])} 50 | 51 | """ 52 | def comparison(client, dimension, value, params \\ []) do 53 | params = Keyword.merge([dimension: dimension, value: value], params) 54 | Base.get(client, build_base_path() <> "/comparison", query: params) 55 | end 56 | 57 | @doc """ 58 | Returns a list of insights for a metric. These are the worst performing values across all breakdowns 59 | sorted by how much they negatively impact a specific metric. 60 | 61 | Returns `{:ok, insights, raw_env}`. 62 | 63 | ## Examples 64 | 65 | iex> client = Mux.client("my_token_id", "my_token_secret") 66 | iex> {:ok, insights, _env} = Mux.Data.Metrics.insights(client, "video_startup_time") 67 | iex> insights 68 | #{inspect(Fixtures.insights()["data"])} 69 | 70 | """ 71 | def insights(client, metric, params \\ []) do 72 | Base.get(client, build_base_path(metric) <> "/insights", query: params) 73 | end 74 | 75 | @doc """ 76 | Returns the overall value for a specific metric, as well as the total view count, watch time, and 77 | the Mux Global metric value for the metric. 78 | 79 | Returns `{:ok, overall_values, raw_env}`. 80 | 81 | ## Examples 82 | 83 | iex> client = Mux.client("my_token_id", "my_token_secret") 84 | iex> {:ok, insights, _env} = Mux.Data.Metrics.overall(client, "video_startup_time") 85 | iex> insights 86 | #{inspect(Fixtures.overall()["data"])} 87 | 88 | """ 89 | def overall(client, metric, params \\ []) do 90 | Base.get(client, build_base_path(metric) <> "/overall", query: params) 91 | end 92 | 93 | @doc """ 94 | Returns time series data for a given metric. 95 | 96 | Returns `{:ok, timeseries, raw_env}`. 97 | 98 | ## Examples 99 | 100 | iex> client = Mux.client("my_token_id", "my_token_secret") 101 | iex> {:ok, timeseries, _env} = Mux.Data.Metrics.timeseries(client, "video_startup_time") 102 | iex> timeseries 103 | #{inspect(Fixtures.timeseries()["data"])} 104 | 105 | """ 106 | def timeseries(client, metric, params \\ []) do 107 | Base.get(client, build_base_path(metric) <> "/timeseries", query: params) 108 | end 109 | 110 | defp build_base_path(), do: "/data/v1/metrics" 111 | defp build_base_path(metric), do: build_base_path() <> "/#{metric}" 112 | end 113 | -------------------------------------------------------------------------------- /lib/mux/data/real_time.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Data.RealTime do 2 | @moduledoc """ 3 | 4 | This module has been deprecated in favor of `Data.Monitoring`. 5 | 6 | This module provides functions that interact with the `real-time` endpoints 7 | 8 | Note, these API documentation links may break periodically as we update documentation titles. 9 | - [Dimensions](https://docs.mux.com/api-reference/data#operation/list-realtime-dimensions) 10 | - [Metrics](https://docs.mux.com/api-reference/data#operation/list-realtime-metrics) 11 | - [Breakdown](https://docs.mux.com/api-reference/data#operation/get-realtime-breakdown) 12 | - [HistogramTimeseries](https://docs.mux.com/api-reference/data#operation/get-realtime-histogram-timeseries) 13 | - [Timeseries](https://docs.mux.com/api-reference/data#operation/get-realtime-timeseries) 14 | """ 15 | alias Mux.{Base, Fixtures} 16 | 17 | @doc """ 18 | This method has been deprecated in favor of `Mux.Data.Monitoring.dimensions`. 19 | 20 | List of available real-time dimensions 21 | 22 | Returns `{:ok, dimensions, raw_env}`. 23 | 24 | ## Examples 25 | 26 | iex> client = Mux.client("my_token_id", "my_token_secret") 27 | iex> {:ok, dimensions, _env} = Mux.Data.RealTime.dimensions(client) 28 | iex> dimensions 29 | #{inspect(Fixtures.realtime_dimensions()["data"])} 30 | 31 | """ 32 | def dimensions(client) do 33 | Base.get(client, build_base_path() <> "/dimensions") 34 | end 35 | 36 | @doc """ 37 | This method has been deprecated in favor of `Mux.Data.Monitoring.metrics`. 38 | 39 | List of available real-time metrics 40 | 41 | Returns `{:ok, metrics, raw_env}`. 42 | 43 | ## Examples 44 | 45 | iex> client = Mux.client("my_token_id", "my_token_secret") 46 | iex> {:ok, metrics, _env} = Mux.Data.RealTime.metrics(client) 47 | iex> metrics 48 | #{inspect(Fixtures.realtime_metrics()["data"])} 49 | 50 | """ 51 | def metrics(client) do 52 | Base.get(client, build_base_path() <> "/metrics") 53 | end 54 | 55 | @doc """ 56 | This method has been deprecated in favor of `Mux.Data.Monitoring.breakdown`. 57 | 58 | Get breakdown information for a specific dimension and metric along with the number of concurrent viewers and negative impact score. 59 | 60 | Returns `{:ok, breakdown, raw_env}`. 61 | 62 | ## Examples 63 | 64 | iex> client = Mux.client("my_token_id", "my_token_secret") 65 | iex> {:ok, breakdown, _env} = Mux.Data.RealTime.breakdown(client, "playback-failure-percentage", dimension: "country", timestamp: 1_547_853_000, filters: ["operating_system:windows"]) 66 | iex> breakdown 67 | #{inspect(Fixtures.realtime_breakdown()["data"])} 68 | 69 | """ 70 | def breakdown(client, metric, params \\ []) do 71 | Base.get(client, build_base_path(metric) <> "/breakdown", query: params) 72 | end 73 | 74 | @doc """ 75 | This method has been deprecated in favor of `Mux.Data.Monitoring.histogram_timeseries`. 76 | 77 | List histogram timeseries information for a specific metric 78 | 79 | Returns `{:ok, histogram_timeseries, raw_env}`. 80 | 81 | ## Examples 82 | 83 | iex> client = Mux.client("my_token_id", "my_token_secret") 84 | iex> {:ok, histogram_timeseries, _env} = Mux.Data.RealTime.histogram_timeseries(client, "video-startup-time", filters: ["operating_system:windows", "country:US"]) 85 | iex> histogram_timeseries 86 | #{inspect(Fixtures.realtime_histogram_timeseries()["data"])} 87 | 88 | """ 89 | def histogram_timeseries(client, metric, params \\ []) do 90 | Base.get(client, build_base_path(metric) <> "/histogram-timeseries", query: params) 91 | end 92 | 93 | @doc """ 94 | This method has been deprecated in favor of `Mux.Data.Monitoring.timeseries`. 95 | 96 | List timeseries information for a specific metric along with the number of concurrent viewers. 97 | 98 | Returns `{:ok, timeseries, raw_env}`. 99 | 100 | ## Examples 101 | 102 | iex> client = Mux.client("my_token_id", "my_token_secret") 103 | iex> {:ok, timeseries, _env} = Mux.Data.RealTime.timeseries(client, "playback-failure-percentage", filters: ["operating_system:windows", "country:US"]) 104 | iex> timeseries 105 | #{inspect(Fixtures.realtime_timeseries()["data"])} 106 | 107 | """ 108 | def timeseries(client, metric, params \\ []) do 109 | Base.get(client, build_base_path(metric) <> "/timeseries", query: params) 110 | end 111 | 112 | defp build_base_path(), do: "/data/v1/realtime" 113 | defp build_base_path(metric), do: build_base_path() <> "/metrics/#{metric}" 114 | end 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Mux Elixir Banner](github-elixir-sdk.png) 2 | 3 | # Mux Elixir 4 | 5 | ![test workflow status](https://github.com/muxinc/mux-elixir/actions/workflows/ci.yml/badge.svg) 6 | 7 | 8 | 9 | Official Mux API wrapper for Elixir projects, supporting both Mux Data and Mux Video. 10 | 11 | [Mux Video](https://mux.com/video) is an API-first platform, powered by data and designed by video experts to make beautiful video possible for every development team. 12 | 13 | [Mux Data](https://mux.com/data) is a platform for monitoring your video streaming performance with just a few lines of code. Get in-depth quality of service analytics on web, mobile, and OTT devices. 14 | 15 | Not familiar with Mux? Check out https://mux.com/ for more information. 16 | 17 | ## Installation 18 | 19 | Add `mux` to your list of dependencies in `mix.exs`: 20 | 21 | ```elixir 22 | def deps do 23 | [ 24 | {:mux, "~> 3.2.2"} 25 | ] 26 | end 27 | ``` 28 | 29 | ## Quickstart 30 | 31 | We'll put our access token in our application configuration. 32 | 33 | ```elixir 34 | # config/dev.exs 35 | config :mux, 36 | access_token_id: "abcd1234", 37 | access_token_secret: "efghijkl" 38 | ``` 39 | 40 | Then use this config to initialize a new client in your application. 41 | 42 | ```elixir 43 | client = Mux.client() 44 | ``` 45 | 46 | You can also pass the access token ID and secret directly to `client/2` function if you'd prefer: 47 | 48 | ```elixir 49 | client = Mux.client("access_token_id", "access_token_secret") 50 | ``` 51 | 52 | Now we can use the client to do anything your heart desires (to do with the Mux API). From here we can 53 | create new videos, manage playback IDs, etc. 54 | 55 | ```elixir 56 | {:ok, asset, raw_env} = Mux.Video.Assets.create(client, %{input: "https://example.com/video.mp4"}); 57 | ``` 58 | 59 | Every successful response will come back with a 3 item tuple starting with `:ok`. The second item 60 | is whatever's in the `data` key, which will typically be the the item you were interacting with. In 61 | the example above, it's a single `asset`. The third item is the raw [Tesla](https://github.com/teamon/tesla) 62 | Env, which is basically the raw response object. This can be useful if you want to get to metadata we 63 | include, such as the timeframe used or the total row count returned, or if you just want to get to 64 | headers such as the request ID for support reasons. 65 | 66 | ## Usage in Phoenix 67 | 68 | Creating a new client before making a request is simple, but you may not want to do it every 69 | single time you need to use a function in a controller. We suggest using `action/2` to initialize 70 | the client and pass that to each of the controller functions. 71 | 72 | ```elixir 73 | def action(conn, _) do 74 | mux_client = Mux.client() # or Mux.client("access_token_id", "access_token_secret") 75 | args = [conn, conn.params, mux_client] 76 | apply(__MODULE__, action_name(conn), args) 77 | end 78 | 79 | def create(conn, params, mux_client) do 80 | # ... 81 | {:ok, asset, _} = mux_client |> Mux.Video.Assets.create(%{input: "http://example.com/input.mp4"}) 82 | # ... 83 | end 84 | ``` 85 | 86 | #### Verifying Webhook Signatures in Phoenix 87 | 88 | Note that when calling `Mux.Webhooks.verify_header/3` in Phoenix you will need to pass in the raw request 89 | body, not the parsed JSON. Phoenix has a nice solution for doing this [example](https://github.com/phoenixframework/phoenix/issues/459#issuecomment-440820663). 90 | 91 | Read more about verifying webhook signatures in [our guide](https://docs.mux.com/docs/webhook-security) 92 | 93 | ```elixir 94 | defmodule MyAppWeb.BodyReader do 95 | def read_body(conn, opts) do 96 | {:ok, body, conn} = Plug.Conn.read_body(conn, opts) 97 | conn = update_in(conn.assigns[:raw_body], &[body | &1 || []]) 98 | {:ok, body, conn} 99 | end 100 | end 101 | 102 | # endpoint.ex 103 | plug Plug.Parsers, 104 | parsers: [:urlencoded, :multipart, :json], 105 | pass: ["*/*"], 106 | body_reader: {MyAppWeb.BodyReader, :read_body, []}, 107 | json_decoder: Phoenix.json_library() 108 | 109 | # controller 110 | signature_header = List.first(get_req_header(conn, "mux-signature")) 111 | raw_body = List.first(conn.assigns.raw_body) 112 | Mux.Webhooks.verify_header(raw_body, signature_header, secret) 113 | ``` 114 | 115 | You will most likely have to store the raw body before it gets parsed and then extract it later and 116 | pass it into `Mux.Webhooks.verify_header/3` 117 | 118 | 119 | 120 | --- 121 | 122 | ## Publishing new versions 123 | 124 | 1. Update version in mix.exs 125 | 1. Update version in README 126 | 1. Commit and open a PR 127 | 1. After code is merged, tag master ex: `git tag v1.7.0` and `git push --tags` 128 | 1. run `mix hex.publish` 129 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, 3 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 4 | "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, 5 | "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, 6 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, 7 | "exvcr": {:hex, :exvcr, "0.11.1", "a5e5f57a67538e032e16cfea6cfb1232314fb146e3ceedf1cde4a11f12fb7a58", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "984a4d52d9e01d5f0e28d45718565a41dffab3ac18e029ae45d42f16a2a58a1d"}, 8 | "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, 9 | "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"}, 10 | "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, 11 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, 12 | "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, 14 | "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, 15 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 16 | "mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "817dec4a7f6edf260258002f99ac8ffaf7a8f395b27bf2d13ec24018beecec8a"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, 18 | "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, 19 | } 20 | -------------------------------------------------------------------------------- /test/mux/video/live_streams_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.LiveStreamsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | doctest Mux.Video.LiveStreams 5 | 6 | @base_url "https://api.mux.com/video/v1/live-streams" 7 | 8 | setup do 9 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 10 | 11 | mock(fn 12 | %{method: :get, url: @base_url} -> 13 | %Tesla.Env{ 14 | status: 200, 15 | body: %{ 16 | "data" => [Mux.Fixtures.live_stream(), Mux.Fixtures.live_stream()] 17 | } 18 | } 19 | 20 | %{method: :post, url: @base_url} -> 21 | %Tesla.Env{ 22 | status: 201, 23 | body: %{ 24 | "data" => Mux.Fixtures.live_stream() 25 | } 26 | } 27 | 28 | %{method: :get, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY"} -> 29 | %Tesla.Env{ 30 | status: 200, 31 | body: %{ 32 | "data" => Mux.Fixtures.live_stream() 33 | } 34 | } 35 | 36 | %{method: :delete, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY"} -> 37 | %Tesla.Env{status: 204, body: ""} 38 | 39 | %{method: :post, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/reset-stream-key"} -> 40 | %Tesla.Env{ 41 | status: 201, 42 | body: %{ 43 | "data" => Mux.Fixtures.live_stream() 44 | } 45 | } 46 | 47 | %{method: :post, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/playback-ids"} -> 48 | %Tesla.Env{ 49 | status: 201, 50 | body: %{ 51 | "data" => Mux.Fixtures.playback_id() 52 | } 53 | } 54 | 55 | %{ 56 | method: :get, 57 | url: 58 | @base_url <> 59 | "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/playback-ids/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE" 60 | } -> 61 | %Tesla.Env{ 62 | status: 200, 63 | body: %{ 64 | "data" => Mux.Fixtures.playback_id() 65 | } 66 | } 67 | 68 | %{method: :put, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/complete"} -> 69 | %Tesla.Env{ 70 | status: 200, 71 | body: "" 72 | } 73 | 74 | %{ 75 | method: :delete, 76 | url: 77 | @base_url <> 78 | "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/playback-ids/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE" 79 | } -> 80 | %Tesla.Env{ 81 | status: 204, 82 | body: "" 83 | } 84 | 85 | %{method: :post, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/simulcast-targets"} -> 86 | %Tesla.Env{ 87 | status: 201, 88 | body: %{ 89 | "data" => Mux.Fixtures.simulcast_target() 90 | } 91 | } 92 | 93 | %{ 94 | method: :get, 95 | url: 96 | @base_url <> 97 | "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/simulcast-targets/vuOfW021mz5QA500wYEQ9SeUYvuYnpFz011mqSvski5T8claN02JN9ve2g" 98 | } -> 99 | %Tesla.Env{ 100 | status: 201, 101 | body: %{ 102 | "data" => Mux.Fixtures.simulcast_target() 103 | } 104 | } 105 | 106 | %{ 107 | method: :delete, 108 | url: 109 | @base_url <> 110 | "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/simulcast-targets/vuOfW021mz5QA500wYEQ9SeUYvuYnpFz011mqSvski5T8claN02JN9ve2g" 111 | } -> 112 | %Tesla.Env{ 113 | status: 204, 114 | body: %{ 115 | "data" => "" 116 | } 117 | } 118 | 119 | %{method: :put, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/enable"} -> 120 | %Tesla.Env{ 121 | status: 200, 122 | body: %{ 123 | "data" => "" 124 | } 125 | } 126 | 127 | %{method: :put, url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/disable"} -> 128 | %Tesla.Env{ 129 | status: 200, 130 | body: %{ 131 | "data" => "" 132 | } 133 | } 134 | 135 | %{ 136 | method: :patch, 137 | url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY" 138 | } -> 139 | %Tesla.Env{ 140 | status: 200, 141 | body: %{ 142 | "data" => Mux.Fixtures.live_stream(:update) 143 | } 144 | } 145 | 146 | %{ 147 | method: :put, 148 | url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/embedded-subtitles" 149 | } -> 150 | %Tesla.Env{ 151 | status: 200, 152 | body: %{ 153 | "data" => Mux.Fixtures.live_stream(:subtitles) 154 | } 155 | } 156 | 157 | %{ 158 | method: :put, 159 | url: @base_url <> "/aA02skpHXoLrbQm49qIzAG6RtewFOcDEY/generated-subtitles" 160 | } -> 161 | %Tesla.Env{ 162 | status: 200, 163 | body: %{ 164 | "data" => Mux.Fixtures.live_stream(:subtitles) 165 | } 166 | } 167 | end) 168 | 169 | {:ok, %{client: client}} 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/mux/video/spaces.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.Spaces do 2 | @moduledoc """ 3 | This module provides functions for managing Spaces in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/spaces) 4 | """ 5 | alias Mux.{Base, Fixtures} 6 | 7 | @path "/video/v1/spaces" 8 | 9 | @doc """ 10 | Create a new space. 11 | 12 | Returns `{:ok, space, %Tesla.Client{}}`. 13 | 14 | ## Examples 15 | 16 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 17 | iex> {:ok, space, _env} = Mux.Video.Spaces.create(client, %{type: "server", passthrough: "example", broadcasts: [%{passthrough: "example", live_stream_id: "vJvFbCojkuSDAAeEK4EddOA01wRqN1mP4", layout: "gallery", background: "https://example.com/background.jpg", resolution: "1920x1080"}]}) 18 | iex> space 19 | #{inspect(Fixtures.space())} 20 | 21 | """ 22 | def create(client, params) do 23 | Base.post(client, @path, params) 24 | end 25 | 26 | @doc """ 27 | List spaces. 28 | 29 | Returns a tuple such as `{:ok, spaces, %Telsa.Env{}}` 30 | 31 | ## Examples 32 | 33 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 34 | iex> {:ok, spaces, _env} = Mux.Video.Spaces.list(client) 35 | iex> spaces 36 | #{inspect([Fixtures.space(), Fixtures.space()])} 37 | 38 | """ 39 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 40 | 41 | @doc """ 42 | Retrieve a space by ID. 43 | 44 | Returns a tuple such as `{:ok, space, %Telsa.Env{}}` 45 | 46 | ## Examples 47 | 48 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 49 | iex> {:ok, space, _env} = Mux.Video.Spaces.get(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk") 50 | iex> space 51 | #{inspect(Fixtures.space())} 52 | 53 | """ 54 | def get(client, space_id, params \\ []) do 55 | Base.get(client, "#{@path}/#{space_id}", query: params) 56 | end 57 | 58 | @doc """ 59 | Delete a space. 60 | 61 | Returns a tuple such as `{:ok, "", %Telsa.Env{}}` 62 | 63 | ## Examples 64 | 65 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 66 | iex> {status, "", _env} = Mux.Video.Spaces.delete(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk") 67 | iex> status 68 | :ok 69 | 70 | """ 71 | def delete(client, space_id, params \\ []) do 72 | Base.delete(client, "#{@path}/#{space_id}", query: params) 73 | end 74 | 75 | @doc """ 76 | Create a new space broadcast. 77 | 78 | Returns a tuple such as `{:ok, broadcast, %Tesla.Env{}}` 79 | 80 | ## Examples 81 | 82 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 83 | iex> {:ok, broadcast, _env} = Mux.Video.Spaces.create_space_broadcast(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", %{passthrough: "example", live_stream_id: "vJvFbCojkuSDAAeEK4EddOA01wRqN1mP4", layout: "gallery", background: "https://example.com/background.jpg", resolution: "1920x1080"}) 84 | iex> broadcast 85 | #{inspect(Fixtures.broadcast())} 86 | 87 | """ 88 | def create_space_broadcast(client, space_id, params) do 89 | Base.post(client, "#{@path}/#{space_id}/broadcasts", params) 90 | end 91 | 92 | @doc """ 93 | Retrieve a space broadcast. 94 | 95 | Returns a tuple such as `{:ok, broadcast, %Tesla.Env{}}` 96 | 97 | ## Examples 98 | 99 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 100 | iex> {:ok, broadcast, _env} = Mux.Video.Spaces.get_space_broadcast(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", "fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ") 101 | iex> broadcast 102 | #{inspect(Fixtures.broadcast())} 103 | 104 | """ 105 | def get_space_broadcast(client, space_id, broadcast_id, params \\ []) do 106 | Base.get(client, "#{@path}/#{space_id}/broadcasts/#{broadcast_id}", query: params) 107 | end 108 | 109 | @doc """ 110 | Delete a space broadcast. 111 | 112 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 113 | 114 | ## Examples 115 | 116 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 117 | iex> {status, "", _env} = Mux.Video.Spaces.delete_space_broadcast(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", "fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ") 118 | iex> status 119 | :ok 120 | 121 | """ 122 | def delete_space_broadcast(client, space_id, broadcast_id, params \\ []) do 123 | Base.delete(client, "#{@path}/#{space_id}/broadcasts/#{broadcast_id}", query: params) 124 | end 125 | 126 | @doc """ 127 | Start a space broadcast. 128 | 129 | Returns a tuple such as `{:ok, %{}, %Tesla.Env{}}` 130 | 131 | ## Examples 132 | 133 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 134 | iex> {status, %{}, _env} = Mux.Video.Spaces.start_space_broadcast(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", "fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ") 135 | iex> status 136 | :ok 137 | 138 | """ 139 | def start_space_broadcast(client, space_id, broadcast_id) do 140 | Base.post(client, "#{@path}/#{space_id}/broadcasts/#{broadcast_id}/start", %{}) 141 | end 142 | 143 | @doc """ 144 | Stop a space broadcast. 145 | 146 | Returns a tuple such as `{:ok, %{}, %Tesla.Env{}}` 147 | 148 | ## Examples 149 | 150 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 151 | iex> {status, %{}, _env} = Mux.Video.Spaces.stop_space_broadcast(client, "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", "fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ") 152 | iex> status 153 | :ok 154 | 155 | """ 156 | def stop_space_broadcast(client, space_id, broadcast_id) do 157 | Base.post(client, "#{@path}/#{space_id}/broadcasts/#{broadcast_id}/stop", %{}) 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/mux/token.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Token do 2 | @moduledoc """ 3 | This module provides helpers for working with Playback IDs with `signed` playback policies. [API Documentation](https://docs.mux.com/docs/security-signed-urls) 4 | """ 5 | 6 | @type signature_type :: :video | :thumbnail | :gif | :storyboard | :stats 7 | @type option :: 8 | {:type, signature_type} 9 | | {:expiration, integer} 10 | | {:token_id, String.t()} 11 | | {:token_secret, String.t()} 12 | | {:params, any} 13 | @type options :: [option] 14 | 15 | @doc """ 16 | Create a signed URL token for a playback ID. 17 | 18 | `options` object can include: 19 | - `options.token_id`: Signing token ID (defaults to `Application.get_env(:mux, :signing_token_id)`) 20 | - `options.token_secret`: Signing token secret (defaults to `Application.get_env(:mux, :signing_token_secret)`) 21 | - `options.type`: Type of signature to create. Defaults to `:video`, options are: `:video, :gif, :thumbnail, :storyboard` 22 | - `options.expiration`: Seconds the token is valid for. Defaults to 7 days from now (604,800) 23 | - `options.params`: Map that includes any additional query params. For thumbnails this would be values like `height` or `time`. 24 | 25 | This method has been deprecated in favor of Mux.Token.sign_playback_id 26 | """ 27 | @spec sign(String.t(), options()) :: String.t() 28 | def sign(playback_id, opts \\ []) do 29 | opts = opts |> default_options() 30 | signer = opts[:token_secret] |> jwt_signer 31 | params = opts[:params] 32 | 33 | claims = %{ 34 | "typ" => "JWT", 35 | "alg" => "RS256", 36 | "kid" => opts[:token_id] 37 | } 38 | 39 | payload = 40 | %{ 41 | "aud" => opts[:type] |> type_to_aud(), 42 | "sub" => playback_id, 43 | "exp" => (DateTime.utc_now() |> DateTime.to_unix()) + opts[:expiration] 44 | } 45 | |> Map.merge(params) 46 | |> Jason.encode!() 47 | 48 | JOSE.JWS.sign(signer, payload, claims) |> JOSE.JWS.compact() |> elem(1) 49 | end 50 | 51 | @doc """ 52 | Create a signed URL token for a playback ID. 53 | 54 | `options` object can include: 55 | - `options.token_id`: Signing token ID (defaults to `Application.get_env(:mux, :signing_token_id)`) 56 | - `options.token_secret`: Signing token secret (defaults to `Application.get_env(:mux, :signing_token_secret)`) 57 | - `options.type`: Type of signature to create. Defaults to `:video`, options are: `:video, :gif, :thumbnail, :storyboard` 58 | - `options.expiration`: Seconds the token is valid for. Defaults to 7 days from now (604,800) 59 | - `options.params`: Map that includes any additional query params. For thumbnails this would be values like `height` or `time`. 60 | """ 61 | @spec sign_playback_id(String.t(), options()) :: String.t() 62 | def sign_playback_id(playback_id, opts \\ []) do 63 | opts = opts |> default_options() 64 | signer = opts[:token_secret] |> jwt_signer 65 | params = opts[:params] 66 | 67 | claims = %{ 68 | "typ" => "JWT", 69 | "alg" => "RS256", 70 | "kid" => opts[:token_id] 71 | } 72 | 73 | payload = 74 | %{ 75 | "aud" => opts[:type] |> type_to_aud(), 76 | "sub" => playback_id, 77 | "exp" => (DateTime.utc_now() |> DateTime.to_unix()) + opts[:expiration] 78 | } 79 | |> Map.merge(params) 80 | |> Jason.encode!() 81 | 82 | JOSE.JWS.sign(signer, payload, claims) |> JOSE.JWS.compact() |> elem(1) 83 | end 84 | 85 | @doc """ 86 | Create a signed URL token for a Space ID. 87 | 88 | `options` object can include: 89 | - `options.token_id`: Signing token ID (defaults to `Application.get_env(:mux, :signing_token_id)`) 90 | - `options.token_secret`: Signing token secret (defaults to `Application.get_env(:mux, :signing_token_secret)`) 91 | - `options.expiration`: Seconds the token is valid for. Defaults to 7 days from now (604,800) 92 | - `options.params`: Map that includes any additional query params. For thumbnails this would be values like `height` or `time`. 93 | """ 94 | @spec sign_space_id(String.t(), options()) :: String.t() 95 | def sign_space_id(space_id, opts \\ []) do 96 | opts = opts |> default_options() 97 | signer = opts[:token_secret] |> jwt_signer 98 | params = opts[:params] 99 | 100 | claims = %{ 101 | "typ" => "JWT", 102 | "alg" => "RS256", 103 | "kid" => opts[:token_id] 104 | } 105 | 106 | payload = 107 | %{ 108 | "aud" => "rt", 109 | "sub" => space_id, 110 | "exp" => (DateTime.utc_now() |> DateTime.to_unix()) + opts[:expiration] 111 | } 112 | |> Map.merge(params) 113 | |> Jason.encode!() 114 | 115 | JOSE.JWS.sign(signer, payload, claims) |> JOSE.JWS.compact() |> elem(1) 116 | end 117 | 118 | def verify(token, opts \\ []) do 119 | %{token_secret: secret} = opts |> default_options() 120 | signer = secret |> jwt_signer() 121 | 122 | JOSE.JWS.verify(signer, token) 123 | end 124 | 125 | defp jwt_signer(secret) do 126 | secret 127 | |> get_private_key() 128 | |> JOSE.JWK.from_pem() 129 | end 130 | 131 | defp default_options(opts) do 132 | application_env = [ 133 | token_id: Application.get_env(:mux, :signing_token_id), 134 | token_secret: Application.get_env(:mux, :signing_token_secret), 135 | expiration: 604_800, 136 | type: :video, 137 | params: %{} 138 | ] 139 | 140 | Keyword.merge(application_env, opts) |> Enum.into(%{}) 141 | end 142 | 143 | defp get_private_key("-----BEGIN RSA PRIVATE KEY-----" <> _ = key), do: key 144 | defp get_private_key(key), do: Base.decode64!(key) 145 | 146 | defp type_to_aud(:video), do: "v" 147 | defp type_to_aud(:thumbnail), do: "t" 148 | defp type_to_aud(:gif), do: "g" 149 | defp type_to_aud(:storyboard), do: "s" 150 | defp type_to_aud(:stats), do: "playback_id" 151 | end 152 | -------------------------------------------------------------------------------- /lib/mux/video/assets.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.Assets do 2 | @moduledoc """ 3 | This module provides functions for managing assets in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/assets) 4 | """ 5 | alias Mux.{Base, Fixtures} 6 | 7 | @path "/video/v1/assets" 8 | 9 | @doc """ 10 | Create a new asset. 11 | 12 | Returns `{:ok, asset, %Tesla.Client{}}`. 13 | 14 | ## Examples 15 | 16 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 17 | iex> Mux.Video.Assets.create(client, %{input: "https://example.com/video.mp4"}) 18 | {:ok, #{inspect(Fixtures.asset(:create))}, #{inspect(Fixtures.tesla_env({:asset, [:create]}))}} 19 | 20 | """ 21 | def create(client, params) do 22 | Base.post(client, @path, params) 23 | end 24 | 25 | @doc """ 26 | List assets. 27 | 28 | Returns a tuple such as `{:ok, assets, %Telsa.Env{}}` 29 | 30 | ## Examples 31 | 32 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 33 | iex> {:ok, assets, _env} = Mux.Video.Assets.list(client) 34 | iex> assets 35 | #{inspect([Fixtures.asset(), Fixtures.asset()])} 36 | 37 | """ 38 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 39 | 40 | @doc """ 41 | Retrieve an asset by ID. 42 | 43 | Returns a tuple such as `{:ok, asset, %Telsa.Env{}}` 44 | 45 | ## Examples 46 | 47 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 48 | iex> {:ok, asset, _env} = Mux.Video.Assets.get(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc") 49 | iex> asset 50 | #{inspect(Fixtures.asset())} 51 | 52 | """ 53 | def get(client, asset_id, options \\ []) do 54 | Base.get(client, "#{@path}/#{asset_id}", query: options) 55 | end 56 | 57 | @doc """ 58 | Delete an asset. 59 | 60 | Returns a tuple such as `{:ok, "", %Telsa.Env{}}` 61 | 62 | ## Examples 63 | 64 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 65 | iex> {status, "", _env} = Mux.Video.Assets.delete(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc") 66 | iex> status 67 | :ok 68 | 69 | """ 70 | def delete(client, asset_id, params \\ []) do 71 | Base.delete(client, "#{@path}/#{asset_id}", query: params) 72 | end 73 | 74 | @doc """ 75 | Retrieve the asset's input info. 76 | 77 | Returns a tuple such as `{:ok, input_info, %Telsa.Env{}}` 78 | 79 | ## Examples 80 | 81 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 82 | iex> {:ok, input_info, _env} = Mux.Video.Assets.input_info(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc") 83 | iex> input_info 84 | [#{inspect(Fixtures.input_info())}] 85 | 86 | """ 87 | def input_info(client, asset_id, params \\ []) do 88 | Base.get(client, "#{@path}/#{asset_id}/input-info", query: params) 89 | end 90 | 91 | @doc """ 92 | Updates an asset, allowing modifications to `passthrough` 93 | 94 | Returns a tuple such as `{:ok, asset, %Tesla.Env{}} 95 | 96 | ## Examples 97 | 98 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 99 | iex> {:ok, asset, _env} = Mux.Video.Assets.update(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{passthrough: "updated_passthrough"}) 100 | iex> asset 101 | #{inspect(Fixtures.asset(:update))} 102 | """ 103 | def update(client, asset_id, params) do 104 | Base.patch(client, "#{@path}/#{asset_id}", params) 105 | end 106 | 107 | @doc """ 108 | Updates an asset's mp4 support 109 | 110 | Returns a tuple such as `{:ok, asset, %Telsa.Env{}}` 111 | 112 | ## Examples 113 | 114 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 115 | iex> {:ok, asset, _env} = Mux.Video.Assets.update_mp4_support(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{mp4_support: "standard"}) 116 | iex> asset 117 | #{inspect(Fixtures.asset())} 118 | 119 | """ 120 | def update_mp4_support(client, asset_id, params) do 121 | Base.put(client, "#{@path}/#{asset_id}/mp4-support", params) 122 | end 123 | 124 | @doc """ 125 | Updates an asset's master access 126 | 127 | Returns a tuple such as `{:ok, asset, %Telsa.Env{}}` 128 | 129 | ## Examples 130 | 131 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 132 | iex> {:ok, asset, _env} = Mux.Video.Assets.update_master_access(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{master_access: "temporary"}) 133 | iex> asset 134 | #{inspect(Fixtures.asset())} 135 | 136 | """ 137 | def update_master_access(client, asset_id, params) do 138 | Base.put(client, "#{@path}/#{asset_id}/master-access", params) 139 | end 140 | 141 | @doc """ 142 | Create a new playback ID. 143 | 144 | Returns `{:ok, playback_id, %Telsa.Env{}}`. 145 | 146 | ## Examples 147 | 148 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 149 | iex> {:ok, playback_id, _env} = Mux.Video.Assets.create_playback_id(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{policy: "public"}) 150 | iex> playback_id 151 | #{inspect(Fixtures.playback_id())} 152 | 153 | """ 154 | def create_playback_id(client, asset_id, params) do 155 | Base.post(client, "#{@path}/#{asset_id}/playback-ids", params) 156 | end 157 | 158 | @doc """ 159 | Retrieve a playback ID. 160 | 161 | Returns `{:ok, playback_id, %Telsa.Env{}}`. 162 | 163 | ## Examples 164 | 165 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 166 | iex> {:ok, playback_id, _env} = Mux.Video.Assets.get_playback_id(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 167 | iex> playback_id 168 | #{inspect(Fixtures.playback_id())} 169 | 170 | """ 171 | def get_playback_id(client, asset_id, playback_id) do 172 | Base.get(client, "#{@path}/#{asset_id}/playback-ids/#{playback_id}") 173 | end 174 | 175 | @doc """ 176 | Delete a playback ID. 177 | 178 | Returns `{:ok, "", %Telsa.Env{}}`. 179 | 180 | ## Examples 181 | 182 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 183 | iex> {status, "", _env} = Mux.Video.Assets.delete_playback_id(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 184 | iex> status 185 | :ok 186 | 187 | """ 188 | def delete_playback_id(client, asset_id, playback_id) do 189 | Base.delete(client, "#{@path}/#{asset_id}/playback-ids/#{playback_id}") 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /test/mux/video/assets_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.AssetsTest do 2 | use ExUnit.Case 3 | import Tesla.Mock 4 | alias Mux.Video.Assets 5 | doctest Mux.Video.Assets 6 | 7 | @base_url "https://api.mux.com" 8 | 9 | setup do 10 | client = Mux.Base.new("token_id", "token_secret", base_url: @base_url) 11 | 12 | mock(fn 13 | %{method: :get, url: @base_url <> "/video/v1/assets"} -> 14 | %Tesla.Env{ 15 | status: 200, 16 | body: %{ 17 | "data" => [ 18 | Mux.Fixtures.asset(), 19 | Mux.Fixtures.asset() 20 | ] 21 | } 22 | } 23 | 24 | %{method: :get, url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc"} -> 25 | %Tesla.Env{ 26 | status: 200, 27 | body: %{ 28 | "data" => Mux.Fixtures.asset() 29 | } 30 | } 31 | 32 | %{ 33 | method: :get, 34 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/input-info" 35 | } -> 36 | %Tesla.Env{ 37 | status: 200, 38 | body: %{ 39 | "data" => [Mux.Fixtures.input_info()] 40 | } 41 | } 42 | 43 | %{method: :post, url: @base_url <> "/video/v1/assets"} -> 44 | %Tesla.Env{ 45 | status: 201, 46 | body: %{ 47 | "data" => Mux.Fixtures.asset(:create) 48 | } 49 | } 50 | 51 | %{ 52 | method: :put, 53 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/mp4-support" 54 | } -> 55 | %Tesla.Env{ 56 | status: 200, 57 | body: %{ 58 | "data" => Mux.Fixtures.asset() 59 | } 60 | } 61 | 62 | %{ 63 | method: :put, 64 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/master-access" 65 | } -> 66 | %Tesla.Env{ 67 | status: 200, 68 | body: %{ 69 | "data" => Mux.Fixtures.asset() 70 | } 71 | } 72 | 73 | %{ 74 | method: :post, 75 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/playback-ids" 76 | } -> 77 | %Tesla.Env{ 78 | status: 201, 79 | body: %{ 80 | "data" => Mux.Fixtures.playback_id() 81 | } 82 | } 83 | 84 | %{ 85 | method: :get, 86 | url: 87 | @base_url <> 88 | "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/playback-ids/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE" 89 | } -> 90 | %Tesla.Env{ 91 | status: 200, 92 | body: %{ 93 | "data" => Mux.Fixtures.playback_id() 94 | } 95 | } 96 | 97 | %{ 98 | method: :delete, 99 | url: 100 | @base_url <> 101 | "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc/playback-ids/FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE" 102 | } -> 103 | %Tesla.Env{status: 204, body: ""} 104 | 105 | %{method: :delete} -> 106 | %Tesla.Env{status: 204, body: ""} 107 | 108 | %{ 109 | method: :patch, 110 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc" 111 | } -> 112 | %Tesla.Env{ 113 | status: 200, 114 | body: %{ 115 | "data" => Mux.Fixtures.asset(:update) 116 | } 117 | } 118 | end) 119 | 120 | {:ok, %{client: client}} 121 | end 122 | 123 | describe "list/2" do 124 | setup do 125 | mock(fn 126 | %{query: [page: 2]} -> 127 | %Tesla.Env{ 128 | status: 200, 129 | body: %{ 130 | "data" => [ 131 | Mux.Fixtures.asset() 132 | ] 133 | } 134 | } 135 | 136 | _ -> 137 | %Tesla.Env{ 138 | status: 200, 139 | body: %{ 140 | "data" => [ 141 | Mux.Fixtures.asset(), 142 | Mux.Fixtures.asset() 143 | ] 144 | } 145 | } 146 | end) 147 | 148 | :ok 149 | end 150 | 151 | test "returns a list of assets", %{client: client} do 152 | {:ok, assets, _} = Assets.list(client) 153 | assert length(assets) == 2 154 | end 155 | 156 | test "takes query params as an option", %{client: client} do 157 | {:ok, assets, _} = Assets.list(client, page: 2) 158 | assert length(assets) == 1 159 | end 160 | 161 | test "returns a third argument that contains the raw Tesla.Env struct", %{client: client} do 162 | assert {:ok, _, %Tesla.Env{}} = Assets.list(client) 163 | end 164 | end 165 | 166 | describe "create/2" do 167 | setup do 168 | mock(fn 169 | %{method: :post} -> 170 | %Tesla.Env{ 171 | status: 201, 172 | body: %{ 173 | "data" => Mux.Fixtures.asset(:create) 174 | } 175 | } 176 | end) 177 | 178 | :ok 179 | end 180 | 181 | test "creates a new asset", %{client: client} do 182 | {:ok, asset, %Tesla.Env{}} = Assets.create(client, %{input: "https://foobar.com/video.mp4"}) 183 | assert asset["status"] === "preparing" 184 | end 185 | end 186 | 187 | describe "get/2" do 188 | setup do 189 | mock(fn 190 | %{method: :get, url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc"} -> 191 | %Tesla.Env{ 192 | status: 201, 193 | body: %{ 194 | "data" => Mux.Fixtures.asset(:created) 195 | } 196 | } 197 | end) 198 | 199 | :ok 200 | end 201 | 202 | test "gets an asset by ID", %{client: client} do 203 | assert {:ok, %{"id" => "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc"}, %Tesla.Env{}} = 204 | Assets.get(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc") 205 | end 206 | end 207 | 208 | describe "update/3" do 209 | setup do 210 | mock(fn 211 | %{ 212 | method: :patch, 213 | url: @base_url <> "/video/v1/assets/00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc" 214 | } -> 215 | %Tesla.Env{ 216 | status: 200, 217 | body: %{ 218 | "data" => Mux.Fixtures.asset(:update) 219 | } 220 | } 221 | end) 222 | 223 | :ok 224 | end 225 | 226 | test "updates an asset by ID", %{client: client} do 227 | assert {:ok, 228 | %{ 229 | "id" => "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", 230 | "passthrough" => "updated_passthrough" 231 | }, 232 | %Tesla.Env{}} = 233 | Assets.update(client, "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", %{ 234 | passthrough: "updated_passthrough" 235 | }) 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /lib/mux/video/live_streams.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Video.LiveStreams do 2 | @moduledoc """ 3 | This module provides functions for managing live streams in Mux Video. [API Documentation](https://docs.mux.com/api-reference/video#tag/live-streams) 4 | """ 5 | alias Mux.{Base, Fixtures} 6 | 7 | @path "/video/v1/live-streams" 8 | 9 | @doc """ 10 | Create a new live stream. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-live-stream) 11 | 12 | Returns `{:ok, live_stream, %Tesla.Env{}}`. 13 | 14 | ## Examples 15 | 16 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 17 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.create(client, %{playback_policy: "public", new_asset_settings: %{playback_policy: "public"}}) 18 | iex> live_stream 19 | #{inspect(Fixtures.live_stream())} 20 | """ 21 | def create(client, params) do 22 | Base.post(client, @path, params) 23 | end 24 | 25 | @doc """ 26 | List all live streams. [API Documentation](https://docs.mux.com/api-reference/video#operation/list-live-streams) 27 | 28 | Returns a tuple such as `{:ok, live_streams, %Tesla.Env{}}` 29 | 30 | ## Examples 31 | 32 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 33 | iex> {:ok, live_streams, _env} = Mux.Video.LiveStreams.list(client) 34 | iex> live_streams 35 | #{inspect([Fixtures.live_stream(), Fixtures.live_stream()])} 36 | 37 | """ 38 | def list(client, params \\ []), do: Base.get(client, @path, query: params) 39 | 40 | @doc """ 41 | Retrieve a live stream by ID. [API Documentation](https://docs.mux.com/api-reference/video#operation/get-live-stream) 42 | 43 | Returns a tuple such as `{:ok, live_stream, %Tesla.Env{}}` 44 | 45 | ## Examples 46 | 47 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 48 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.get(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 49 | iex> live_stream 50 | #{inspect(Fixtures.live_stream())} 51 | 52 | """ 53 | def get(client, live_stream_id, options \\ []) do 54 | Base.get(client, "#{@path}/#{live_stream_id}", query: options) 55 | end 56 | 57 | @doc """ 58 | Delete a live stream. [API Documentation](https://docs.mux.com/api-reference/video#operation/delete-live-stream) 59 | 60 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 61 | 62 | ## Examples 63 | 64 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 65 | iex> {status, _data, _env} = Mux.Video.LiveStreams.delete(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 66 | iex> status 67 | :ok 68 | 69 | """ 70 | def delete(client, live_stream_id) do 71 | Base.delete(client, "#{@path}/#{live_stream_id}") 72 | end 73 | 74 | @doc """ 75 | Signal a live stream is finished. [API Documentation](https://docs.mux.com/api-reference/video#operation/signal-live-stream-complete) 76 | 77 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 78 | 79 | ## Examples 80 | 81 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 82 | iex> {status, _, _env} = Mux.Video.LiveStreams.signal_complete(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 83 | iex> status 84 | :ok 85 | 86 | """ 87 | def signal_complete(client, live_stream_id) do 88 | Base.put(client, "#{@path}/#{live_stream_id}/complete", %{}) 89 | end 90 | 91 | @doc """ 92 | Reset a live stream key if you want to immediately stop the current stream key 93 | from working and create a new stream key that can be used for future broadcasts. 94 | [API Documentation](https://docs.mux.com/api-reference/video#operation/reset-stream-key) 95 | 96 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 97 | 98 | ## Examples 99 | 100 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 101 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.reset_stream_key(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 102 | iex> live_stream 103 | #{inspect(Fixtures.live_stream())} 104 | 105 | """ 106 | def reset_stream_key(client, live_stream_id) do 107 | Base.post(client, "#{@path}/#{live_stream_id}/reset-stream-key", %{}) 108 | end 109 | 110 | @doc """ 111 | Create a live stream playback ID. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-live-stream-playback-id) 112 | 113 | Returns a tuple such as `{:ok, playback_ids, %Tesla.Env{}}` 114 | 115 | ## Examples 116 | 117 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 118 | iex> {:ok, playback_id, _env} = Mux.Video.LiveStreams.create_playback_id(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", %{policy: "public"}) 119 | iex> playback_id 120 | #{inspect(Fixtures.playback_id())} 121 | 122 | """ 123 | def create_playback_id(client, live_stream_id, params) do 124 | Base.post(client, "#{@path}/#{live_stream_id}/playback-ids", params) 125 | end 126 | 127 | @doc """ 128 | Retrieve a playback ID. 129 | 130 | Returns `{:ok, playback_id, %Telsa.Env{}}`. 131 | 132 | ## Examples 133 | 134 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 135 | iex> {:ok, playback_id, _env} = Mux.Video.LiveStreams.get_playback_id(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 136 | iex> playback_id 137 | #{inspect(Fixtures.playback_id())} 138 | 139 | """ 140 | def get_playback_id(client, live_stream_id, playback_id) do 141 | Base.get(client, "#{@path}/#{live_stream_id}/playback-ids/#{playback_id}") 142 | end 143 | 144 | @doc """ 145 | Delete a live stream playback ID. [API Documentation](https://docs.mux.com/api-reference/video#operation/delete-live-stream-playback-id) 146 | 147 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 148 | 149 | ## Examples 150 | 151 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 152 | iex> {status, _, _env} = Mux.Video.LiveStreams.delete_playback_id(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE") 153 | iex> status 154 | :ok 155 | 156 | """ 157 | def delete_playback_id(client, live_stream_id, playback_id) do 158 | Base.delete(client, "#{@path}/#{live_stream_id}/playback-ids/#{playback_id}") 159 | end 160 | 161 | @doc """ 162 | Create a live stream simulcast target. [API Documentation](https://docs.mux.com/api-reference/video#operation/create-live-stream-simulcast-target) 163 | 164 | Returns a tuple such as `{:ok, simulcast_target, %Tesla.Env{}}` 165 | 166 | ## Examples 167 | 168 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 169 | iex> {:ok, simulcast_target, _env} = Mux.Video.LiveStreams.create_simulcast_target(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", %{url: "rtmp://live.example.com/app", stream_key: "abcdefgh"}) 170 | iex> simulcast_target 171 | #{inspect(Fixtures.simulcast_target())} 172 | 173 | """ 174 | def create_simulcast_target(client, live_stream_id, params) do 175 | Base.post(client, "#{@path}/#{live_stream_id}/simulcast-targets", params) 176 | end 177 | 178 | @doc """ 179 | Retrieve a live stream simulcast target. [API Documentation](https://docs.mux.com/api-reference/video#operation/get-live-stream-simulcast-target) 180 | 181 | Returns a tuple such as `{:ok, simulcast_target, %Tesla.Env{}}` 182 | 183 | ## Examples 184 | 185 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 186 | iex> {:ok, simulcast_target, _env} = Mux.Video.LiveStreams.get_simulcast_target(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", "vuOfW021mz5QA500wYEQ9SeUYvuYnpFz011mqSvski5T8claN02JN9ve2g") 187 | iex> simulcast_target 188 | #{inspect(Fixtures.simulcast_target())} 189 | 190 | """ 191 | def get_simulcast_target(client, live_stream_id, simulcast_target_id) do 192 | Base.get(client, "#{@path}/#{live_stream_id}/simulcast-targets/#{simulcast_target_id}") 193 | end 194 | 195 | @doc """ 196 | Delete a live stream simulcast target. [API Documentation](https://docs.mux.com/api-reference/video#operation/delete-live-stream-simulcast-target) 197 | 198 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 199 | 200 | ## Examples 201 | 202 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 203 | iex> {status, _, _env} = Mux.Video.LiveStreams.delete_simulcast_target(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", "vuOfW021mz5QA500wYEQ9SeUYvuYnpFz011mqSvski5T8claN02JN9ve2g") 204 | iex> status 205 | :ok 206 | 207 | """ 208 | def delete_simulcast_target(client, live_stream_id, simulcast_target_id) do 209 | Base.delete(client, "#{@path}/#{live_stream_id}/simulcast-targets/#{simulcast_target_id}") 210 | end 211 | 212 | @doc """ 213 | Enable a live stream is finished. [API Documentation](https://docs.mux.com/api-reference/video#operation/enable-live-stream) 214 | 215 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 216 | 217 | ## Examples 218 | 219 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 220 | iex> {status, _, _env} = Mux.Video.LiveStreams.enable(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 221 | iex> status 222 | :ok 223 | 224 | """ 225 | def enable(client, live_stream_id) do 226 | Base.put(client, "#{@path}/#{live_stream_id}/enable", %{}) 227 | end 228 | 229 | @doc """ 230 | Disable a live stream is finished. [API Documentation](https://docs.mux.com/api-reference/video#operation/disable-live-stream) 231 | 232 | Returns a tuple such as `{:ok, "", %Tesla.Env{}}` 233 | 234 | ## Examples 235 | 236 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 237 | iex> {status, _, _env} = Mux.Video.LiveStreams.disable(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY") 238 | iex> status 239 | :ok 240 | 241 | """ 242 | def disable(client, live_stream_id) do 243 | Base.put(client, "#{@path}/#{live_stream_id}/disable", %{}) 244 | end 245 | 246 | @doc """ 247 | Updates a live stream. See https://docs.mux.com/api-reference/video#operation/update-live-stream for 248 | the values that are allowed. 249 | 250 | Returns a tuple such as `{:ok, live_stream, %Tesla.Env{}} 251 | 252 | ## Examples 253 | 254 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 255 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.update(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", %{passthrough: "updated_passthrough", latency_mode: "low", max_continuous_duration: 21600, reconnect_window: 30}) 256 | iex> live_stream 257 | #{inspect(Fixtures.live_stream(:update))} 258 | """ 259 | def update(client, live_stream_id, params) do 260 | Base.patch(client, "#{@path}/#{live_stream_id}", params) 261 | end 262 | 263 | @doc """ 264 | Updates a live stream's embedded subtitles 265 | 266 | Returns a tuple such as `{:ok, live_stream, %Tesla.Env{}} 267 | 268 | ## Examples 269 | 270 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 271 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.update_embedded_subtitles(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", %{embedded_subtitles: %{name: "English CC", passthrough: "Example", language_code: "en", language_channel: "cc1"}}) 272 | iex> live_stream 273 | #{inspect(Fixtures.live_stream(:subtitles))} 274 | """ 275 | def update_embedded_subtitles(client, live_stream_id, params) do 276 | Base.put(client, "#{@path}/#{live_stream_id}/embedded-subtitles", params) 277 | end 278 | 279 | @doc """ 280 | Updates a live stream's generated subtitles 281 | 282 | Returns a tuple such as `{:ok, live_stream, %Tesla.Env{}} 283 | 284 | ## Examples 285 | 286 | iex> client = Mux.Base.new("my_token_id", "my_token_secret") 287 | iex> {:ok, live_stream, _env} = Mux.Video.LiveStreams.update_generated_subtitles(client, "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", %{generated_subtitles: %{name: "English generated", passthrough: "Example", language: "en"}}) 288 | iex> live_stream 289 | #{inspect(Fixtures.live_stream(:subtitles))} 290 | """ 291 | def update_generated_subtitles(client, live_stream_id, params) do 292 | Base.put(client, "#{@path}/#{live_stream_id}/generated-subtitles", params) 293 | end 294 | end 295 | -------------------------------------------------------------------------------- /lib/mux/support/fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Mux.Fixtures do 2 | @moduledoc false 3 | 4 | def asset(a \\ nil) 5 | 6 | def asset(:create) do 7 | %{ 8 | "status" => "preparing", 9 | "playback_ids" => [ 10 | %{ 11 | "policy" => "public", 12 | "id" => "CypWdvOIUrxjI7RlRAbVm01fGxFMO6wfH" 13 | } 14 | ], 15 | "id" => "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", 16 | "created_at" => "1521503163" 17 | } 18 | end 19 | 20 | def asset(:update) do 21 | asset() 22 | |> Map.put("passthrough", "updated_passthrough") 23 | end 24 | 25 | def asset(_) do 26 | %{ 27 | "tracks" => [ 28 | %{ 29 | "type" => "video", 30 | "max_width" => 1920, 31 | "max_height" => 1080, 32 | "max_frame_rate" => 29.97, 33 | "id" => "7wVkNSIVRAIKB1oj6jezHK1BATAXuwy3", 34 | "duration" => 23.8238 35 | }, 36 | %{ 37 | "type" => "audio", 38 | "max_channels" => 2, 39 | "max_channel_layout" => "stereo", 40 | "id" => "PnA02RYkGox02I01NDr02yAzp5qqAiOS401VgQO1pg00gByzk", 41 | "duration" => 23.872 42 | } 43 | ], 44 | "status" => "ready", 45 | "playback_ids" => [ 46 | %{ 47 | "policy" => "public", 48 | "id" => "CypWdvOIUrxjI7RlRAbVm01fGxFMO6wfH" 49 | } 50 | ], 51 | "max_stored_resolution" => "HD", 52 | "max_stored_frame_rate" => 29.97, 53 | "mp4_support" => "none", 54 | "master_access" => "none", 55 | "id" => "00ecNLnqiG8v00TLqqeZ00uCE5wCAaO3kKc", 56 | "duration" => 23.872, 57 | "created_at" => "1521503163", 58 | "aspect_ratio" => "16:9" 59 | } 60 | end 61 | 62 | def live_stream(:update) do 63 | live_stream() 64 | |> Map.put("passthrough", "updated_passthrough") 65 | |> Map.put("latency_mode", "low") 66 | |> Map.put("reconnect_window", 30) 67 | |> Map.put("max_continuous_duration", 21600) 68 | end 69 | 70 | def live_stream(:subtitles) do 71 | live_stream() 72 | |> Map.put("embedded_subtitles", %{ 73 | "name" => "English CC", 74 | "language_code" => "en", 75 | "language_channel" => "cc1", 76 | "passthrough" => "Example" 77 | }) 78 | end 79 | 80 | def live_stream() do 81 | %{ 82 | "stream_key" => "54676b58-6b19-5acb-f5bf-3aa35222efc6", 83 | "status" => "idle", 84 | "reconnect_window" => 60, 85 | "playback_ids" => [ 86 | %{ 87 | "policy" => "public", 88 | "id" => "iOkfIGstt4u1eYdFcPKGTWKz75Acpv3w" 89 | } 90 | ], 91 | "new_asset_settings" => %{ 92 | "playback_policies" => ["public"] 93 | }, 94 | "id" => "aA02skpHXoLrbQm49qIzAG6RtewFOcDEY", 95 | "created_at" => "1557338261" 96 | } 97 | end 98 | 99 | def playback_id_full do 100 | %{ 101 | "policy" => "public", 102 | "object" => %{ 103 | "type" => "live_stream", 104 | "id" => "ZXs9U1Wqr3C4GBVCOYRQOA00lLtijnVhehJIb8tlDzL00" 105 | }, 106 | "id" => "7IxC7stYLro5Z4nEs97J02OkLEKFME6mvhnuRJybhRKU" 107 | } 108 | end 109 | 110 | def input_info() do 111 | %{ 112 | "file" => %{ 113 | "container_format" => "mov,mp4,m4a,3gp,3g2,mj2", 114 | "tracks" => [ 115 | %{ 116 | "duration" => 23.8238, 117 | "encoding" => "h264", 118 | "frame_rate" => 29.97, 119 | "height" => 1080, 120 | "type" => "video", 121 | "width" => 1920 122 | }, 123 | %{ 124 | "channels" => 2, 125 | "duration" => 23.872, 126 | "encoding" => "aac", 127 | "sample_rate" => 48000, 128 | "type" => "audio" 129 | } 130 | ] 131 | }, 132 | "settings" => %{ 133 | "url" => "https://storage.googleapis.com/muxdemofiles/mux-video-intro.mp4" 134 | } 135 | } 136 | end 137 | 138 | def playback_id do 139 | %{ 140 | "policy" => "public", 141 | "id" => "FRDDXsjcNgD013rx1M4CDunZ86xkq8A02hfF3b6XAa7iE" 142 | } 143 | end 144 | 145 | def track do 146 | %{ 147 | "type" => "text", 148 | "text_type" => "subtitles", 149 | "status" => "preparing", 150 | "passthrough" => "English", 151 | "name" => "English", 152 | "language_code" => "en", 153 | "id" => "2" 154 | } 155 | end 156 | 157 | def simulcast_target do 158 | %{ 159 | "id" => "vuOfW021mz5QA500wYEQ9SeUYvuYnpFz011mqSvski5T8claN02JN9ve2g", 160 | "url" => "rtmp://live.example1.com/app", 161 | "stream_key" => "abcdefgh", 162 | "passthrough" => "Example 1" 163 | } 164 | end 165 | 166 | def delivery_usage do 167 | %{ 168 | "passthrough" => "Example 1", 169 | "delivered_seconds" => 2040.533333, 170 | "created_at" => "1566324794", 171 | "asset_state" => "ready", 172 | "asset_id" => "XwYTXo41yZD2xDIOKp2p00", 173 | "asset_duration" => 480.533333 174 | } 175 | end 176 | 177 | def playback_restriction do 178 | %{ 179 | "updated_at" => "1653174155", 180 | "referrer" => %{ 181 | "allowed_domains" => [ 182 | "*.example.com" 183 | ], 184 | "allow_no_referrer" => true 185 | }, 186 | "id" => "uP6cf00TE5HUvfdEmI6PR01vXQgZEjydC3", 187 | "created_at" => "1653174155" 188 | } 189 | end 190 | 191 | def space do 192 | %{ 193 | "broadcasts" => [ 194 | broadcast() 195 | ], 196 | "created_at" => "1653342466", 197 | "id" => "xe00FkgJMdZrYQ001VC53bd01lf9ADs6YWk", 198 | "passthrough" => "example", 199 | "status" => "idle", 200 | "type" => "server" 201 | } 202 | end 203 | 204 | def broadcast do 205 | %{ 206 | "background" => "https://example.com/background.jpg", 207 | "id" => "fZw6qjWmKLmjfi0200NBzsgGrXZImT3KiJ", 208 | "layout" => "gallery", 209 | "live_stream_id" => "vJvFbCojkuSDAAeEK4EddOA01wRqN1mP4", 210 | "resolution" => "1920x1080", 211 | "status" => "idle" 212 | } 213 | end 214 | 215 | def breakdown do 216 | %{ 217 | "total_row_count" => 2, 218 | "timeframe" => [ 219 | 1_516_241_988, 220 | 1_516_501_188 221 | ], 222 | "data" => [ 223 | %{ 224 | "views" => 3, 225 | "value" => 3500, 226 | "total_watch_time" => 7500, 227 | "negative_impact" => 1, 228 | "field" => "mac" 229 | }, 230 | %{ 231 | "views" => 2, 232 | "value" => 1000, 233 | "total_watch_time" => 3500, 234 | "negative_impact" => 2, 235 | "field" => "windows" 236 | } 237 | ] 238 | } 239 | end 240 | 241 | def comparison do 242 | %{ 243 | "total_row_count" => nil, 244 | "timeframe" => [ 245 | 1_516_241_921, 246 | 1_516_501_121 247 | ], 248 | "data" => [ 249 | %{ 250 | "watch_time" => 32000, 251 | "view_count" => 2, 252 | "name" => "totals" 253 | }, 254 | %{ 255 | "value" => 0.25, 256 | "type" => "score", 257 | "name" => "Overall Score", 258 | "metric" => "viewer_experience_score" 259 | }, 260 | %{ 261 | "value" => 1, 262 | "type" => "score", 263 | "name" => "Playback Failure Score", 264 | "metric" => "playback_failure_score", 265 | "items" => [ 266 | %{ 267 | "value" => 0, 268 | "type" => "percentage", 269 | "name" => "Playback Failure Percentage", 270 | "metric" => "playback_failure_percentage" 271 | } 272 | ] 273 | }, 274 | %{ 275 | "value" => 1, 276 | "type" => "score", 277 | "name" => "Startup Time Score", 278 | "metric" => "startup_time_score", 279 | "items" => [ 280 | %{ 281 | "value" => 1000, 282 | "type" => "milliseconds", 283 | "name" => "Video Startup Time (median)", 284 | "metric" => "video_startup_time", 285 | "measurement" => "median" 286 | }, 287 | %{ 288 | "value" => 1000, 289 | "type" => "milliseconds", 290 | "name" => "Video Startup Time (95th %)", 291 | "metric" => "video_startup_time", 292 | "measurement" => "95th" 293 | }, 294 | %{ 295 | "value" => nil, 296 | "type" => "milliseconds", 297 | "name" => "Player Startup Time (median)", 298 | "metric" => "player_startup_time", 299 | "measurement" => "median" 300 | }, 301 | %{ 302 | "value" => nil, 303 | "type" => "milliseconds", 304 | "name" => "Player Startup Time (95th %)", 305 | "metric" => "player_startup_time", 306 | "measurement" => "95th" 307 | }, 308 | %{ 309 | "value" => nil, 310 | "type" => "milliseconds", 311 | "name" => "Page Load Time (median)", 312 | "metric" => "page_load_time", 313 | "measurement" => "median" 314 | }, 315 | %{ 316 | "value" => nil, 317 | "type" => "milliseconds", 318 | "name" => "Page Load Time (95th %)", 319 | "metric" => "page_load_time", 320 | "measurement" => "95th" 321 | }, 322 | %{ 323 | "value" => nil, 324 | "type" => "milliseconds", 325 | "name" => "Aggregate Startup Time (median)", 326 | "metric" => "aggregate_startup_time", 327 | "measurement" => "median" 328 | }, 329 | %{ 330 | "value" => nil, 331 | "type" => "milliseconds", 332 | "name" => "Aggregate Startup Time (95th %)", 333 | "metric" => "aggregate_startup_time", 334 | "measurement" => "95th" 335 | }, 336 | %{ 337 | "value" => nil, 338 | "type" => "milliseconds", 339 | "name" => "Seek Latency", 340 | "metric" => "seek_latency" 341 | }, 342 | %{ 343 | "value" => 0, 344 | "type" => "percentage", 345 | "name" => "Exits Before Video Start", 346 | "metric" => "exits_before_video_start" 347 | } 348 | ] 349 | }, 350 | %{ 351 | "value" => 0.25, 352 | "type" => "score", 353 | "name" => "Rebuffer Score", 354 | "metric" => "rebuffer_score", 355 | "items" => [ 356 | %{ 357 | "value" => 0, 358 | "type" => "percentage", 359 | "name" => "Rebuffer Percentage", 360 | "metric" => "rebuffer_percentage" 361 | }, 362 | %{ 363 | "value" => 13.125, 364 | "type" => "per_minute", 365 | "name" => "Rebuffer Frequency", 366 | "metric" => "rebuffer_frequency" 367 | }, 368 | %{ 369 | "value" => 0, 370 | "type" => "milliseconds", 371 | "name" => "Rebuffer Duration (median)", 372 | "metric" => "rebuffer_duration", 373 | "measurement" => "median" 374 | }, 375 | %{ 376 | "value" => 0, 377 | "type" => "milliseconds", 378 | "name" => "Rebuffer Duration (95th %)", 379 | "metric" => "rebuffer_duration", 380 | "measurement" => "95th" 381 | }, 382 | %{ 383 | "value" => 2, 384 | "type" => "number", 385 | "name" => "Rebuffer Count (median)", 386 | "metric" => "rebuffer_count", 387 | "measurement" => "median" 388 | }, 389 | %{ 390 | "value" => 5, 391 | "type" => "number", 392 | "name" => "Rebuffer Count (95th %)", 393 | "metric" => "rebuffer_count", 394 | "measurement" => "95th" 395 | } 396 | ] 397 | }, 398 | %{ 399 | "value" => nil, 400 | "type" => "score", 401 | "name" => "Video Quality Score", 402 | "metric" => "video_quality_score", 403 | "items" => [ 404 | %{ 405 | "value" => nil, 406 | "type" => "percentage", 407 | "name" => "Upscale Percentage (median)", 408 | "metric" => "upscale_percentage", 409 | "measurement" => "median" 410 | }, 411 | %{ 412 | "value" => nil, 413 | "type" => "percentage", 414 | "name" => "Upscale Percentage (95th %)", 415 | "metric" => "upscale_percentage", 416 | "measurement" => "95th" 417 | }, 418 | %{ 419 | "value" => nil, 420 | "type" => "percentage", 421 | "name" => "Upscale Percentage (average)", 422 | "metric" => "upscale_percentage", 423 | "measurement" => "avg" 424 | }, 425 | %{ 426 | "value" => nil, 427 | "type" => "percentage", 428 | "name" => "Downscale Percentage (median)", 429 | "metric" => "downscale_percentage", 430 | "measurement" => "median" 431 | }, 432 | %{ 433 | "value" => nil, 434 | "type" => "percentage", 435 | "name" => "Downscale Percentage (95th %)", 436 | "metric" => "downscale_percentage", 437 | "measurement" => "95th" 438 | }, 439 | %{ 440 | "value" => nil, 441 | "type" => "percentage", 442 | "name" => "Downscale Percentage (average)", 443 | "metric" => "downscale_percentage", 444 | "measurement" => "avg" 445 | }, 446 | %{ 447 | "value" => nil, 448 | "type" => "percentage", 449 | "name" => "Max Upscale Percentage (median)", 450 | "metric" => "max_upscale_percentage", 451 | "measurement" => "median" 452 | }, 453 | %{ 454 | "value" => nil, 455 | "type" => "percentage", 456 | "name" => "Max Upscale Percentage (95th %)", 457 | "metric" => "max_upscale_percentage", 458 | "measurement" => "95th" 459 | }, 460 | %{ 461 | "value" => nil, 462 | "type" => "percentage", 463 | "name" => "Max Downscale Percentage (median)", 464 | "metric" => "max_downscale_percentage", 465 | "measurement" => "median" 466 | }, 467 | %{ 468 | "value" => nil, 469 | "type" => "percentage", 470 | "name" => "Max Downscale Percentage (95th %)", 471 | "metric" => "max_downscale_percentage", 472 | "measurement" => "95th" 473 | } 474 | ] 475 | } 476 | ] 477 | } 478 | end 479 | 480 | def exports() do 481 | %{ 482 | "total_row_count" => 2, 483 | "timeframe" => [ 484 | 1_516_328_401, 485 | 1_516_414_801 486 | ], 487 | "data" => [ 488 | "https://s3.amazonaws.com/mux-data-exports-test/10942/2017_10_1.csv.gz?signature=asdf1234", 489 | "https://s3.amazonaws.com/mux-data-exports-test/10942/2017_10_2.csv.gz?signature=asdf1234" 490 | ] 491 | } 492 | end 493 | 494 | def view_exports() do 495 | %{ 496 | "total_row_count" => 7, 497 | "timeframe" => [ 498 | 1_626_296_941, 499 | 1_626_383_341 500 | ], 501 | "data" => [ 502 | %{ 503 | "files" => [ 504 | %{ 505 | "version" => 2, 506 | "type" => "csv", 507 | "path" => 508 | "https://s3.amazonaws.com/mux-data-exports/1/2021_01_03.csv.gz?...signature..." 509 | } 510 | ], 511 | "export_date" => "2021-01-03" 512 | }, 513 | %{ 514 | "files" => [ 515 | %{ 516 | "version" => 2, 517 | "type" => "csv", 518 | "path" => 519 | "https://s3.amazonaws.com/mux-data-exports/1/2021_01_02.csv.gz?...signature..." 520 | } 521 | ], 522 | "export_date" => "2021-01-02" 523 | }, 524 | %{ 525 | "files" => [ 526 | %{ 527 | "version" => 2, 528 | "type" => "csv", 529 | "path" => 530 | "https://s3.amazonaws.com/mux-data-exports/1/2021_01_01.csv.gz?...signature..." 531 | } 532 | ], 533 | "export_date" => "2021-01-01" 534 | } 535 | ] 536 | } 537 | end 538 | 539 | def filters() do 540 | %{ 541 | "total_row_count" => nil, 542 | "timeframe" => [ 543 | 1_516_328_397, 544 | 1_516_414_797 545 | ], 546 | "data" => %{ 547 | "basic" => [ 548 | "browser", 549 | "country", 550 | "operating_system", 551 | "player_software", 552 | "player_software_version", 553 | "source_hostname", 554 | "source_type", 555 | "stream_type", 556 | "video_title" 557 | ], 558 | "advanced" => [ 559 | "asn", 560 | "browser_version", 561 | "cdn", 562 | "experiment_name", 563 | "operating_system_version", 564 | "player_name", 565 | "player_version", 566 | "preroll_ad_asset_hostname", 567 | "preroll_ad_tag_hostname", 568 | "preroll_played", 569 | "preroll_requested", 570 | "sub_property_id", 571 | "video_series" 572 | ] 573 | } 574 | } 575 | end 576 | 577 | def dimensions() do 578 | %{ 579 | "total_row_count" => nil, 580 | "timeframe" => [ 581 | 1_516_328_397, 582 | 1_516_414_797 583 | ], 584 | "data" => %{ 585 | "basic" => [ 586 | "browser", 587 | "country", 588 | "operating_system", 589 | "player_software", 590 | "player_software_version", 591 | "source_hostname", 592 | "source_type", 593 | "stream_type", 594 | "video_title" 595 | ], 596 | "advanced" => [ 597 | "asn", 598 | "browser_version", 599 | "cdn", 600 | "experiment_name", 601 | "operating_system_version", 602 | "player_name", 603 | "player_version", 604 | "preroll_ad_asset_hostname", 605 | "preroll_ad_tag_hostname", 606 | "preroll_played", 607 | "preroll_requested", 608 | "sub_property_id", 609 | "video_series" 610 | ] 611 | } 612 | } 613 | end 614 | 615 | def filters(:browser) do 616 | %{ 617 | "total_row_count" => 2, 618 | "timeframe" => [ 619 | 1_516_241_996, 620 | 1_516_501_196 621 | ], 622 | "data" => [ 623 | %{ 624 | "value" => "Safari", 625 | "total_count" => 2 626 | }, 627 | %{ 628 | "value" => "Chrome", 629 | "total_count" => 1 630 | } 631 | ] 632 | } 633 | end 634 | 635 | def dimensions(:browser) do 636 | %{ 637 | "total_row_count" => 2, 638 | "timeframe" => [ 639 | 1_516_241_996, 640 | 1_516_501_196 641 | ], 642 | "data" => [ 643 | %{ 644 | "value" => "Safari", 645 | "total_count" => 2 646 | }, 647 | %{ 648 | "value" => "Chrome", 649 | "total_count" => 1 650 | } 651 | ] 652 | } 653 | end 654 | 655 | def incidents() do 656 | %{ 657 | "total_row_count" => 1, 658 | "timeframe" => [ 659 | 1_563_237_968, 660 | 1_563_324_368 661 | ], 662 | "data" => [ 663 | %{ 664 | "threshold" => 50, 665 | "status" => "open", 666 | "started_at" => "2019-07-17T00:46:08.344Z", 667 | "severity" => "alert", 668 | "sample_size_unit" => "views", 669 | "sample_size" => 100, 670 | "resolved_at" => nil, 671 | "notifications" => [], 672 | "notification_rules" => [], 673 | "measurement" => "error_rate", 674 | "measured_value_on_close" => nil, 675 | "measured_value" => 55.7, 676 | "incident_key" => "country=US", 677 | "impact" => "*30 views* have been affected so far at a rate of *60 per hour*", 678 | "id" => "pid1083", 679 | "error_description" => "This is a message for this crazy error", 680 | "description" => 681 | "Overall error-rate is significantly high (55.7%) due to an error of *This is a message for this crazy error*", 682 | "breakdowns" => [ 683 | %{ 684 | "value" => "957", 685 | "name" => "error_type_id", 686 | "id" => "pid1077" 687 | } 688 | ], 689 | "affected_views_per_hour_on_open" => 30, 690 | "affected_views_per_hour" => 60, 691 | "affected_views" => 30 692 | } 693 | ] 694 | } 695 | end 696 | 697 | def incident() do 698 | %{ 699 | "total_row_count" => nil, 700 | "timeframe" => [ 701 | 1_563_237_972, 702 | 1_563_324_372 703 | ], 704 | "data" => %{ 705 | "threshold" => 50, 706 | "status" => "open", 707 | "started_at" => "2019-07-16T23:46:12.047Z", 708 | "severity" => "alert", 709 | "sample_size_unit" => "views", 710 | "sample_size" => 100, 711 | "resolved_at" => nil, 712 | "notifications" => [], 713 | "notification_rules" => [], 714 | "measurement" => "error_rate", 715 | "measured_value_on_close" => nil, 716 | "measured_value" => 55.7, 717 | "incident_key" => "country=US", 718 | "impact" => nil, 719 | "id" => "ABCD1234", 720 | "error_description" => "This is a message for this crazy error", 721 | "description" => 722 | "Overall error-rate is significantly high (55.7%) due to an error of *This is a message for this crazy error*", 723 | "breakdowns" => [ 724 | %{ 725 | "value" => "989", 726 | "name" => "error_type_id", 727 | "id" => "pid1949" 728 | } 729 | ], 730 | "affected_views_per_hour_on_open" => nil, 731 | "affected_views_per_hour" => nil, 732 | "affected_views" => nil 733 | } 734 | } 735 | end 736 | 737 | def related_incidents() do 738 | %{ 739 | "total_row_count" => nil, 740 | "timeframe" => [ 741 | 1_563_237_971, 742 | 1_563_324_371 743 | ], 744 | "data" => [ 745 | %{ 746 | "threshold" => 50, 747 | "status" => "open", 748 | "started_at" => "2019-07-16T23:46:11.183Z", 749 | "severity" => "alert", 750 | "sample_size_unit" => "views", 751 | "sample_size" => 100, 752 | "resolved_at" => nil, 753 | "notifications" => [], 754 | "notification_rules" => [], 755 | "measurement" => "error_rate", 756 | "measured_value_on_close" => nil, 757 | "measured_value" => 55.7, 758 | "incident_key" => "country=US", 759 | "impact" => "*30 views* have been affected so far at a rate of *25 per hour*", 760 | "id" => "pid1759", 761 | "error_description" => "This is a message for this crazy error", 762 | "description" => 763 | "Overall error-rate is significantly high (55.7%) due to an error of *This is a message for this crazy error*", 764 | "breakdowns" => [ 765 | %{ 766 | "value" => "983", 767 | "name" => "error_type_id", 768 | "id" => "pid1753" 769 | } 770 | ], 771 | "affected_views_per_hour_on_open" => 30, 772 | "affected_views_per_hour" => 25, 773 | "affected_views" => 30 774 | } 775 | ] 776 | } 777 | end 778 | 779 | def insights() do 780 | %{ 781 | "total_row_count" => 2, 782 | "timeframe" => [ 783 | 1_500_389_723, 784 | 1_500_648_923 785 | ], 786 | "data" => [ 787 | %{ 788 | "total_watch_time" => 10000, 789 | "total_views" => 5, 790 | "total_row_count" => 1, 791 | "negative_impact_score" => 0.6, 792 | "metric" => 4000, 793 | "filter_value" => "US", 794 | "filter_column" => "country" 795 | } 796 | ] 797 | } 798 | end 799 | 800 | def overall() do 801 | %{ 802 | "total_row_count" => nil, 803 | "timeframe" => [ 804 | 1_516_241_946, 805 | 1_516_501_146 806 | ], 807 | "data" => %{ 808 | "value" => 0.8333333333333334, 809 | "total_watch_time" => 3000, 810 | "total_views" => 3, 811 | "global_value" => nil 812 | } 813 | } 814 | end 815 | 816 | def signature(:sign) do 817 | "abcdewefsdllkjejosoeifjoseifjosiejlsekfjsloeifjselofijsoeifjsoeifj" 818 | end 819 | 820 | def signing_key(a \\ nil) 821 | 822 | def signing_key(:create) do 823 | %{ 824 | "private_key" => "thisisaverysecretkeythatinreallifewouldbealotlonger==", 825 | "id" => "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C", 826 | "created_at" => "1540438441" 827 | } 828 | end 829 | 830 | def signing_key(_) do 831 | %{ 832 | "id" => "3kXq01SS00BQZqHHIq1egKAhuf7urAc400C", 833 | "created_at" => "1540438441" 834 | } 835 | end 836 | 837 | def timeseries() do 838 | %{ 839 | "total_row_count" => 73, 840 | "timeframe" => [ 841 | 1_516_241_947, 842 | 1_516_501_147 843 | ], 844 | "data" => [ 845 | [ 846 | "2018-01-19T02:00:00.000Z", 847 | 0, 848 | 2 849 | ], 850 | [ 851 | "2018-01-19T03:00:00.000Z", 852 | 1, 853 | 3 854 | ] 855 | ] 856 | } 857 | end 858 | 859 | def upload(a \\ nil) 860 | 861 | def upload(:create) do 862 | %{ 863 | "url" => 864 | "https://storage.googleapis.com/video-storage-us-east1-uploads/OOTbA00CpWh6OgwV3asF00IvD2STk22UXM?Expires=1545157644&GoogleAccessId=mux-direct-upload%40mux-cloud.iam.gserviceaccount.com&Signature=bloopblop", 865 | "timeout" => 3600, 866 | "status" => "waiting", 867 | "new_asset_settings" => %{ 868 | "playback_policies" => [ 869 | "public" 870 | ] 871 | }, 872 | "id" => "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM", 873 | "cors_origin" => "http://localhost:8080" 874 | } 875 | end 876 | 877 | def upload(:cancel) do 878 | %{ 879 | "url" => 880 | "https://storage.googleapis.com/video-storage-us-east1-uploads/OOTbA00CpWh6OgwV3asF00IvD2STk22UXM?Expires=1545157644&GoogleAccessId=mux-direct-upload%40mux-cloud.iam.gserviceaccount.com&Signature=bloopblop", 881 | "timeout" => 3600, 882 | "status" => "cancelled", 883 | "new_asset_settings" => %{ 884 | "playback_policies" => [ 885 | "public" 886 | ] 887 | }, 888 | "id" => "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM", 889 | "cors_origin" => "http://localhost:8080" 890 | } 891 | end 892 | 893 | def upload(_) do 894 | %{ 895 | "url" => 896 | "https://storage.googleapis.com/video-storage-us-east1-uploads/OOTbA00CpWh6OgwV3asF00IvD2STk22UXM?Expires=1545157644&GoogleAccessId=mux-direct-upload%40mux-cloud.iam.gserviceaccount.com&Signature=bloopblop", 897 | "timeout" => 3600, 898 | "status" => "waiting", 899 | "new_asset_settings" => %{ 900 | "playback_policies" => [ 901 | "public" 902 | ] 903 | }, 904 | "id" => "OOTbA00CpWh6OgwV3asF00IvD2STk22UXM", 905 | "cors_origin" => "http://localhost:8080" 906 | } 907 | end 908 | 909 | def transcription_vocabulary() do 910 | %{ 911 | "created_at" => "1657242248", 912 | "id" => "ANZLqMO4E01TQW01SyFJfrdZzvjMVuyYqE", 913 | "name" => "API Vocabulary", 914 | "phrases" => ["Mux", "Live Stream", "Playback ID"], 915 | "updated_at" => "1657242248" 916 | } 917 | end 918 | 919 | def transcription_vocabulary(:update) do 920 | %{ 921 | "created_at" => "1657242248", 922 | "id" => "ANZLqMO4E01TQW01SyFJfrdZzvjMVuyYqE", 923 | "name" => "New API Vocabulary", 924 | "phrases" => ["Mux", "Live Stream", "Playback ID", "New phrase"], 925 | "updated_at" => "1657242420" 926 | } 927 | end 928 | 929 | def video_views() do 930 | %{ 931 | "total_row_count" => 2, 932 | "timeframe" => [ 933 | 1_516_241_921, 934 | 1_516_501_121 935 | ], 936 | "data" => [ 937 | %{ 938 | "viewer_os_family" => nil, 939 | "viewer_application_name" => "Chrome", 940 | "view_start" => "2018-01-19T02 =>18 =>41.940Z", 941 | "view_end" => "2018-01-19T02 =>18 =>41.940Z", 942 | "video_title" => nil, 943 | "total_row_count" => 2, 944 | "player_error_message" => nil, 945 | "player_error_code" => nil, 946 | "id" => "DEM1z65UMx6Ldcgq9tDR9tkJNTqlE5eNlN4", 947 | "error_type_id" => 1087, 948 | "country_code" => nil 949 | }, 950 | %{ 951 | "viewer_os_family" => nil, 952 | "viewer_application_name" => "Chrome", 953 | "view_start" => "2018-01-19T02 =>18 =>41.940Z", 954 | "view_end" => "2018-01-19T02 =>18 =>41.940Z", 955 | "video_title" => nil, 956 | "total_row_count" => 2, 957 | "player_error_message" => nil, 958 | "player_error_code" => nil, 959 | "id" => "k8n4aklUyrRDekILDWta1qSJqNFpYB7N50", 960 | "error_type_id" => 1088, 961 | "country_code" => nil 962 | } 963 | ] 964 | } 965 | end 966 | 967 | def video_view() do 968 | %{ 969 | "total_row_count" => 1, 970 | "timeframe" => [ 971 | 1_516_241_921, 972 | 1_516_501_121 973 | ], 974 | "data" => %{ 975 | "view_seek_duration" => nil, 976 | "video_language" => nil, 977 | "viewer_os_family" => nil, 978 | "viewer_user_agent" => nil, 979 | "player_error_code" => nil, 980 | "player_height" => nil, 981 | "player_poster" => nil, 982 | "time_to_first_frame" => nil, 983 | "player_source_type" => nil, 984 | "video_stream_type" => nil, 985 | "cdn" => nil, 986 | "buffering_rate" => nil, 987 | "metro" => nil, 988 | "player_instance_id" => "123", 989 | "viewer_application_version" => "8", 990 | "updated_at" => "2018-01-20T02:19:15.000Z", 991 | "player_source_host_name" => nil, 992 | "preroll_played" => false, 993 | "player_source_duration" => nil, 994 | "view_total_downscaling" => nil, 995 | "player_source_stream_type" => nil, 996 | "continent_code" => nil, 997 | "view_error_id" => nil, 998 | "page_load_time" => nil, 999 | "video_variant_id" => nil, 1000 | "view_id" => "123", 1001 | "watch_time" => nil, 1002 | "isp" => nil, 1003 | "inserted_at" => "2018-01-20T02:19:15.000Z", 1004 | "viewer_application_name" => "Safari", 1005 | "exit_before_video_start" => false, 1006 | "view_seek_count" => nil, 1007 | "player_software" => nil, 1008 | "player_preload" => false, 1009 | "asn" => nil, 1010 | "view_end" => "2018-01-19T02:18:41.940Z", 1011 | "events" => [], 1012 | "video_startup_preroll_request_time" => nil, 1013 | "view_total_content_playback_time" => nil, 1014 | "buffering_duration" => nil, 1015 | "requests_for_first_preroll" => nil, 1016 | "player_width" => nil, 1017 | "page_url" => "http://example.com/foo/bar", 1018 | "preroll_ad_tag_hostname" => nil, 1019 | "session_id" => "123", 1020 | "viewer_os_architecture" => nil, 1021 | "page_type" => nil, 1022 | "view_max_downscale_percentage" => nil, 1023 | "preroll_ad_asset_hostname" => nil, 1024 | "video_variant_name" => nil, 1025 | "experiment_name" => nil, 1026 | "watched" => false, 1027 | "mux_api_version" => nil, 1028 | "player_load_time" => nil, 1029 | "preroll_requested" => false, 1030 | "region" => nil, 1031 | "player_error_message" => nil, 1032 | "country_code" => nil, 1033 | "player_source_domain" => nil, 1034 | "longitude" => nil, 1035 | "player_name" => nil, 1036 | "video_producer" => nil, 1037 | "video_series" => nil, 1038 | "country_name" => nil, 1039 | "rebuffer_percentage" => nil, 1040 | "used_fullscreen" => false, 1041 | "video_encoding_variant" => nil, 1042 | "player_language" => nil, 1043 | "viewer_device_manufacturer" => nil, 1044 | "view_start" => "2018-01-19T02:18:41.940Z", 1045 | "latitude" => nil, 1046 | "viewer_device_category" => nil, 1047 | "video_id" => nil, 1048 | "player_source_width" => nil, 1049 | "property_id" => 10185, 1050 | "city" => nil, 1051 | "player_software_version" => nil, 1052 | "player_autoplay" => false, 1053 | "video_duration" => nil, 1054 | "buffering_count" => nil, 1055 | "view_max_upscale_percentage" => nil, 1056 | "platform_summary" => "Safari", 1057 | "viewer_application_engine" => nil, 1058 | "mux_embed_version" => nil, 1059 | "error_type_id" => nil, 1060 | "id" => "k8n4aklUyrRDekILDWta1qSJqNFpYB7N50", 1061 | "player_version" => nil, 1062 | "player_startup_time" => nil, 1063 | "player_source_url" => nil, 1064 | "view_total_upscaling" => nil, 1065 | "short_time" => " 2:18am UTC 2018-01-19", 1066 | "viewer_user_id" => nil, 1067 | "player_mux_plugin_version" => nil, 1068 | "video_title" => nil, 1069 | "mux_viewer_id" => "123", 1070 | "viewer_device_name" => nil, 1071 | "viewer_os_version" => nil, 1072 | "video_content_type" => nil, 1073 | "player_view_count" => nil, 1074 | "player_source_height" => nil, 1075 | "video_startup_preroll_load_time" => nil, 1076 | "platform_description" => nil 1077 | } 1078 | } 1079 | end 1080 | 1081 | def errors() do 1082 | %{ 1083 | "total_row_count" => nil, 1084 | "timeframe" => [ 1085 | 1_516_328_328, 1086 | 1_516_414_728 1087 | ], 1088 | "data" => [ 1089 | %{ 1090 | "percentage" => 0.6666666666666666, 1091 | "notes" => "This is a really crazy note", 1092 | "message" => "This is a message for this crazy error", 1093 | "last_seen" => "2018-01-20T01:18:48.054Z", 1094 | "id" => 1121, 1095 | "description" => 1096 | "If we're going to understand this error, first we need to understand life itself.", 1097 | "count" => 2, 1098 | "code" => 1 1099 | }, 1100 | %{ 1101 | "percentage" => 0.3333333333333333, 1102 | "notes" => "This is a really crazy note", 1103 | "message" => "This is a message for this crazy error", 1104 | "last_seen" => "2018-01-19T23:18:48.054Z", 1105 | "id" => 1120, 1106 | "description" => 1107 | "If we're going to understand this error, first we need to understand life itself.", 1108 | "count" => 1, 1109 | "code" => 3 1110 | } 1111 | ] 1112 | } 1113 | end 1114 | 1115 | def realtime_dimensions() do 1116 | %{ 1117 | "total_row_count" => nil, 1118 | "timeframe" => [ 1119 | 1_584_577_180, 1120 | 1_584_663_580 1121 | ], 1122 | "data" => [ 1123 | %{ 1124 | "name" => "asn", 1125 | "display_name" => "ASN" 1126 | }, 1127 | %{ 1128 | "name" => "cdn", 1129 | "display_name" => "CDN" 1130 | }, 1131 | %{ 1132 | "name" => "country", 1133 | "display_name" => "Country" 1134 | }, 1135 | %{ 1136 | "name" => "operating_system", 1137 | "display_name" => "Operating system" 1138 | }, 1139 | %{ 1140 | "name" => "player_name", 1141 | "display_name" => "Player name" 1142 | }, 1143 | %{ 1144 | "name" => "region", 1145 | "display_name" => "Region / State" 1146 | }, 1147 | %{ 1148 | "name" => "stream_type", 1149 | "display_name" => "Stream type" 1150 | }, 1151 | %{ 1152 | "name" => "sub_property_id", 1153 | "display_name" => "Sub property ID" 1154 | }, 1155 | %{ 1156 | "name" => "video_series", 1157 | "display_name" => "Video series" 1158 | }, 1159 | %{ 1160 | "name" => "video_title", 1161 | "display_name" => "Video title" 1162 | } 1163 | ] 1164 | } 1165 | end 1166 | 1167 | def realtime_metrics() do 1168 | %{ 1169 | "total_row_count" => nil, 1170 | "timeframe" => [ 1171 | 1_584_577_184, 1172 | 1_584_663_584 1173 | ], 1174 | "data" => [ 1175 | %{ 1176 | "name" => "current-concurrent-viewers", 1177 | "display_name" => "Current Concurrent Viewers (CCV)" 1178 | }, 1179 | %{ 1180 | "name" => "current-rebuffering-percentage", 1181 | "display_name" => "Current Rebuffering Percentage" 1182 | }, 1183 | %{ 1184 | "name" => "exits-before-video-start", 1185 | "display_name" => "Exits Before Video Start" 1186 | }, 1187 | %{ 1188 | "name" => "playback-failure-percentage", 1189 | "display_name" => "Playback Failure Percentage" 1190 | }, 1191 | %{ 1192 | "name" => "video-startup-time", 1193 | "display_name" => "Video Startup Time" 1194 | } 1195 | ] 1196 | } 1197 | end 1198 | 1199 | def realtime_breakdown() do 1200 | %{ 1201 | "total_row_count" => nil, 1202 | "timeframe" => [ 1203 | 1_547_853_000, 1204 | 1_547_853_000 1205 | ], 1206 | "data" => [ 1207 | %{ 1208 | "value" => "AR", 1209 | "negative_impact" => 3, 1210 | "metric_value" => 0, 1211 | "concurrent_viewers" => 1 1212 | } 1213 | ] 1214 | } 1215 | end 1216 | 1217 | def realtime_histogram_timeseries() do 1218 | %{ 1219 | "total_row_count" => nil, 1220 | "timeframe" => [ 1221 | 1_582_591_920, 1222 | 1_582_593_660 1223 | ], 1224 | "meta" => %{ 1225 | "buckets" => [ 1226 | %{ 1227 | "start" => 0, 1228 | "end" => 100 1229 | }, 1230 | %{ 1231 | "start" => 100, 1232 | "end" => 500 1233 | }, 1234 | %{ 1235 | "start" => 500, 1236 | "end" => 1000 1237 | }, 1238 | %{ 1239 | "start" => 1000, 1240 | "end" => 2000 1241 | }, 1242 | %{ 1243 | "start" => 2000, 1244 | "end" => 5000 1245 | }, 1246 | %{ 1247 | "start" => 5000, 1248 | "end" => 10000 1249 | }, 1250 | %{ 1251 | "start" => 10000, 1252 | "end" => nil 1253 | } 1254 | ], 1255 | "bucket_unit" => "milliseconds" 1256 | }, 1257 | "data" => [ 1258 | %{ 1259 | "timestamp" => "2020-02-25T00:52:00Z", 1260 | "sum" => 76, 1261 | "p95" => 6809, 1262 | "median" => 425, 1263 | "max_percentage" => 0.27631578947368424, 1264 | "bucket_values" => [ 1265 | %{ 1266 | "percentage" => 0.25, 1267 | "count" => 19 1268 | }, 1269 | %{ 1270 | "percentage" => 0.27631578947368424, 1271 | "count" => 21 1272 | }, 1273 | %{ 1274 | "percentage" => 0.19736842105263158, 1275 | "count" => 15 1276 | }, 1277 | %{ 1278 | "percentage" => 0.14473684210526316, 1279 | "count" => 11 1280 | }, 1281 | %{ 1282 | "percentage" => 0.05263157894736842, 1283 | "count" => 4 1284 | }, 1285 | %{ 1286 | "percentage" => 0.05263157894736842, 1287 | "count" => 4 1288 | }, 1289 | %{ 1290 | "percentage" => 0.02631578947368421, 1291 | "count" => 2 1292 | } 1293 | ], 1294 | "average" => 1446.328947368421 1295 | } 1296 | ] 1297 | } 1298 | end 1299 | 1300 | def realtime_timeseries() do 1301 | %{ 1302 | "total_row_count" => nil, 1303 | "timeframe" => [ 1304 | 1_582_591_905, 1305 | 1_582_593_700 1306 | ], 1307 | "data" => [ 1308 | %{ 1309 | "value" => 0.0597809346162238, 1310 | "date" => "2020-02-25T00:51:45Z", 1311 | "concurrent_viewers" => 477 1312 | }, 1313 | %{ 1314 | "value" => 0.059590005296620834, 1315 | "date" => "2020-02-25T00:51:50Z", 1316 | "concurrent_viewers" => 487 1317 | } 1318 | ] 1319 | } 1320 | end 1321 | 1322 | def monitoring_dimensions() do 1323 | %{ 1324 | "total_row_count" => nil, 1325 | "timeframe" => [ 1326 | 1_584_577_180, 1327 | 1_584_663_580 1328 | ], 1329 | "data" => [ 1330 | %{ 1331 | "name" => "asn", 1332 | "display_name" => "ASN" 1333 | }, 1334 | %{ 1335 | "name" => "cdn", 1336 | "display_name" => "CDN" 1337 | }, 1338 | %{ 1339 | "name" => "country", 1340 | "display_name" => "Country" 1341 | }, 1342 | %{ 1343 | "name" => "operating_system", 1344 | "display_name" => "Operating system" 1345 | }, 1346 | %{ 1347 | "name" => "player_name", 1348 | "display_name" => "Player name" 1349 | }, 1350 | %{ 1351 | "name" => "region", 1352 | "display_name" => "Region / State" 1353 | }, 1354 | %{ 1355 | "name" => "stream_type", 1356 | "display_name" => "Stream type" 1357 | }, 1358 | %{ 1359 | "name" => "sub_property_id", 1360 | "display_name" => "Sub property ID" 1361 | }, 1362 | %{ 1363 | "name" => "video_series", 1364 | "display_name" => "Video series" 1365 | }, 1366 | %{ 1367 | "name" => "video_title", 1368 | "display_name" => "Video title" 1369 | } 1370 | ] 1371 | } 1372 | end 1373 | 1374 | def monitoring_metrics() do 1375 | %{ 1376 | "total_row_count" => nil, 1377 | "timeframe" => [ 1378 | 1_584_577_184, 1379 | 1_584_663_584 1380 | ], 1381 | "data" => [ 1382 | %{ 1383 | "name" => "current-concurrent-viewers", 1384 | "display_name" => "Current Concurrent Viewers (CCV)" 1385 | }, 1386 | %{ 1387 | "name" => "current-rebuffering-percentage", 1388 | "display_name" => "Current Rebuffering Percentage" 1389 | }, 1390 | %{ 1391 | "name" => "exits-before-video-start", 1392 | "display_name" => "Exits Before Video Start" 1393 | }, 1394 | %{ 1395 | "name" => "playback-failure-percentage", 1396 | "display_name" => "Playback Failure Percentage" 1397 | }, 1398 | %{ 1399 | "name" => "video-startup-time", 1400 | "display_name" => "Video Startup Time" 1401 | } 1402 | ] 1403 | } 1404 | end 1405 | 1406 | def monitoring_breakdown() do 1407 | %{ 1408 | "total_row_count" => nil, 1409 | "timeframe" => [ 1410 | 1_547_853_000, 1411 | 1_547_853_000 1412 | ], 1413 | "data" => [ 1414 | %{ 1415 | "value" => "AR", 1416 | "negative_impact" => 3, 1417 | "metric_value" => 0, 1418 | "concurrent_viewers" => 1 1419 | } 1420 | ] 1421 | } 1422 | end 1423 | 1424 | def monitoring_histogram_timeseries() do 1425 | %{ 1426 | "total_row_count" => nil, 1427 | "timeframe" => [ 1428 | 1_582_591_920, 1429 | 1_582_593_660 1430 | ], 1431 | "meta" => %{ 1432 | "buckets" => [ 1433 | %{ 1434 | "start" => 0, 1435 | "end" => 100 1436 | }, 1437 | %{ 1438 | "start" => 100, 1439 | "end" => 500 1440 | }, 1441 | %{ 1442 | "start" => 500, 1443 | "end" => 1000 1444 | }, 1445 | %{ 1446 | "start" => 1000, 1447 | "end" => 2000 1448 | }, 1449 | %{ 1450 | "start" => 2000, 1451 | "end" => 5000 1452 | }, 1453 | %{ 1454 | "start" => 5000, 1455 | "end" => 10000 1456 | }, 1457 | %{ 1458 | "start" => 10000, 1459 | "end" => nil 1460 | } 1461 | ], 1462 | "bucket_unit" => "milliseconds" 1463 | }, 1464 | "data" => [ 1465 | %{ 1466 | "timestamp" => "2020-02-25T00:52:00Z", 1467 | "sum" => 76, 1468 | "p95" => 6809, 1469 | "median" => 425, 1470 | "max_percentage" => 0.27631578947368424, 1471 | "bucket_values" => [ 1472 | %{ 1473 | "percentage" => 0.25, 1474 | "count" => 19 1475 | }, 1476 | %{ 1477 | "percentage" => 0.27631578947368424, 1478 | "count" => 21 1479 | }, 1480 | %{ 1481 | "percentage" => 0.19736842105263158, 1482 | "count" => 15 1483 | }, 1484 | %{ 1485 | "percentage" => 0.14473684210526316, 1486 | "count" => 11 1487 | }, 1488 | %{ 1489 | "percentage" => 0.05263157894736842, 1490 | "count" => 4 1491 | }, 1492 | %{ 1493 | "percentage" => 0.05263157894736842, 1494 | "count" => 4 1495 | }, 1496 | %{ 1497 | "percentage" => 0.02631578947368421, 1498 | "count" => 2 1499 | } 1500 | ], 1501 | "average" => 1446.328947368421 1502 | } 1503 | ] 1504 | } 1505 | end 1506 | 1507 | def monitoring_timeseries() do 1508 | %{ 1509 | "total_row_count" => nil, 1510 | "timeframe" => [ 1511 | 1_582_591_905, 1512 | 1_582_593_700 1513 | ], 1514 | "data" => [ 1515 | %{ 1516 | "value" => 0.0597809346162238, 1517 | "date" => "2020-02-25T00:51:45Z", 1518 | "concurrent_viewers" => 477 1519 | }, 1520 | %{ 1521 | "value" => 0.059590005296620834, 1522 | "date" => "2020-02-25T00:51:50Z", 1523 | "concurrent_viewers" => 487 1524 | } 1525 | ] 1526 | } 1527 | end 1528 | 1529 | def tesla_env({fixture_name, args}) do 1530 | %Tesla.Env{ 1531 | __client__: nil, 1532 | __module__: nil, 1533 | body: %{ 1534 | "data" => apply(__MODULE__, fixture_name, args) 1535 | }, 1536 | headers: [], 1537 | method: nil, 1538 | opts: [], 1539 | query: [], 1540 | status: 201, 1541 | url: "" 1542 | } 1543 | end 1544 | end 1545 | --------------------------------------------------------------------------------