Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion lib/mave_core/media/storage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ defmodule MaveCore.Media.Storage do
@copy_tmp_root "mave-storage-copy"
@default_object_acl_profiles ["eu", "eu_3"]
@space_hash_pattern ~r/^[a-z0-9]{5}$/
@cacheable_media_content_types ["video/mp4", "audio/mp4"]
# Versioned output keys (`{embed}/v{n}/...`) are never rewritten with different
# bytes, so browsers and CDNs may keep them without revalidating. Unversioned
# legacy keys can be replaced in place and get a short lifetime instead.
@versioned_media_cache_control "public, max-age=31536000, immutable"
@unversioned_media_cache_control "public, max-age=86400"
@versioned_media_key_pattern ~r{(?:^|/)v[1-9][0-9]*/}

@doc """
Returns the bucket name for a given space hash.
Expand Down Expand Up @@ -392,6 +399,7 @@ defmodule MaveCore.Media.Storage do
{"content-type", content_type},
{"content-length", Integer.to_string(size)}
]
|> maybe_add_media_headers(path, content_type, opts)
|> maybe_add_public_read_header(storage_profile, opts)

put_object_with_retry(
Expand All @@ -418,6 +426,7 @@ defmodule MaveCore.Media.Storage do
headers =
[{"content-type", content_type}]
|> maybe_add_content_length_header(opts)
|> maybe_add_media_headers(path, content_type, opts)
|> maybe_add_public_read_header(storage_profile, opts)

put_object_with_retry(bucket, path, storage_profile, headers, fn -> body end)
Expand Down Expand Up @@ -635,6 +644,7 @@ defmodule MaveCore.Media.Storage do
{"content-type", content_type},
{"content-length", Integer.to_string(content_length)}
]
|> maybe_add_media_headers(path, content_type, public: true)
|> maybe_add_public_read_header(storage_profile, public: true)

with {:ok, endpoint} <- required_storage_config(config, :endpoint),
Expand Down Expand Up @@ -2164,9 +2174,12 @@ defmodule MaveCore.Media.Storage do
end

defp initiate_multipart_upload(bucket, path, storage_profile, content_type, opts \\ []) do
public? = Keyword.get(opts, :public, true)

headers =
[{"content-type", content_type}]
|> maybe_add_public_read_header(storage_profile, public: Keyword.get(opts, :public, true))
|> maybe_add_media_headers(path, content_type, public: public?)
|> maybe_add_public_read_header(storage_profile, public: public?)

result =
build_req(bucket, storage_profile)
Expand Down Expand Up @@ -3210,6 +3223,26 @@ defmodule MaveCore.Media.Storage do
end
end

# Public MP4 and fMP4 outputs are served straight from storage or a CDN, so the
# caching and inline-playback headers are stored as object metadata.
defp maybe_add_media_headers(headers, path, content_type, opts) do
if Keyword.get(opts, :public, false) and content_type in @cacheable_media_content_types do
headers ++
[
{"cache-control", media_cache_control(path)},
{"content-disposition", "inline"}
]
else
headers
end
end

defp media_cache_control(path) do
if Regex.match?(@versioned_media_key_pattern, path),
do: @versioned_media_cache_control,
else: @unversioned_media_cache_control
end

defp copy_source_header(bucket, path) do
encoded_path =
path
Expand Down
7 changes: 6 additions & 1 deletion lib/mave_core/playback/media.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ defmodule MaveCore.Playback.Media do
ttl = expires_at - System.system_time(:second)
bucket = Storage.bucket_for_space(embed.space.hash, embed.space.region)

# Public objects carry long-lived shared caching metadata. A signed URL must
# not outlive its signature in a shared cache.
query = [{"response-cache-control", "private, max-age=#{ttl}"}]

if ttl > 0 do
case storage().presigned_get_url(bucket, embed.hash <> "/" <> path, embed.space.region,
expires: ttl
expires: ttl,
query_params: query
) do
{:ok, url} -> {:redirect, url}
{:error, _reason} -> {:error, :storage_unavailable}
Expand Down
81 changes: 78 additions & 3 deletions test/mave_core/media/storage_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ defmodule MaveCore.Media.StorageTest do
assert conn.method == "PUT"
assert conn.request_path == "/space-mett/manifest.json"
assert get_req_header(conn, "x-amz-acl") == ["public-read"]
assert get_req_header(conn, "cache-control") == []
assert get_req_header(conn, "content-disposition") == []

send_resp(conn, 200, "")
end)
Expand All @@ -382,6 +384,72 @@ defmodule MaveCore.Media.StorageTest do
)
end

test "public MP4 outputs store caching and inline playback headers" do
Req.default_options(plug: {Req.Test, __MODULE__})

Application.put_env(:mave_core, :storage_providers, %{
"mett" => [
access_key_id: "minioadmin",
secret_access_key: "minioadmin",
endpoint: "http://localhost:9010",
region: "fr-par",
bucket_prefix: "space-"
]
})

Req.Test.expect(__MODULE__, 3, fn conn ->
expected_cache_control =
case conn.request_path do
"/space-mett/embed/v2/" <> _rest -> "public, max-age=31536000, immutable"
"/space-mett/embed/h264_sd.mp4" -> "public, max-age=86400"
end

assert get_req_header(conn, "cache-control") == [expected_cache_control]
assert get_req_header(conn, "content-disposition") == ["inline"]

send_resp(conn, 200, "")
end)

assert {:ok, ""} =
Storage.put_public("space-mett", "embed/v2/h264_sd.mp4", "mp4", "video/mp4", "mett")

assert {:ok, ""} =
Storage.put_public(
"space-mett",
"embed/v2/audio_hls/segment_000.m4s",
"m4s",
"audio/mp4",
"mett"
)

assert {:ok, ""} =
Storage.put_public("space-mett", "embed/h264_sd.mp4", "mp4", "video/mp4", "mett")
end

test "private MP4 writes do not store public caching headers" do
Req.default_options(plug: {Req.Test, __MODULE__})

Application.put_env(:mave_core, :storage_providers, %{
"mett" => [
access_key_id: "minioadmin",
secret_access_key: "minioadmin",
endpoint: "http://localhost:9010",
region: "fr-par",
bucket_prefix: "space-"
]
})

Req.Test.expect(__MODULE__, fn conn ->
assert get_req_header(conn, "cache-control") == []
assert get_req_header(conn, "content-disposition") == []

send_resp(conn, 200, "")
end)

assert {:ok, ""} =
Storage.put("space-mett", "embed/v2/source.mp4", "mp4", "video/mp4", "mett")
end

test "put_public retries transient transport failures" do
Req.default_options(plug: {Req.Test, __MODULE__})

Expand Down Expand Up @@ -1274,6 +1342,8 @@ defmodule MaveCore.Media.StorageTest do
assert conn.query_string == "uploads"
assert get_req_header(conn, "content-type") == ["video/mp4"]
assert get_req_header(conn, "x-amz-acl") == ["public-read"]
assert get_req_header(conn, "cache-control") == ["public, max-age=86400"]
assert get_req_header(conn, "content-disposition") == ["inline"]

send_resp(
conn,
Expand Down Expand Up @@ -1329,6 +1399,7 @@ defmodule MaveCore.Media.StorageTest do
Req.Test.expect(__MODULE__, fn conn ->
assert conn.method == "POST"
assert get_req_header(conn, "x-amz-acl") == []
assert get_req_header(conn, "cache-control") == []

send_resp(
conn,
Expand Down Expand Up @@ -1362,7 +1433,7 @@ defmodule MaveCore.Media.StorageTest do
assert {:ok, %{"url" => url, "headers" => headers}} =
Storage.presigned_put_url(
"space-qingb",
"embed/h264_hd_hls/segment_000.m4s",
"embed/v3/h264_hd_hls/segment_000.m4s",
"qingb",
"video/mp4",
12_345
Expand All @@ -1373,11 +1444,15 @@ defmodule MaveCore.Media.StorageTest do

assert uri.scheme == "https"
assert uri.host == "storage.example.test"
assert uri.path == "/space-qingb/embed/h264_hd_hls/segment_000.m4s"
assert uri.path == "/space-qingb/embed/v3/h264_hd_hls/segment_000.m4s"
assert is_binary(params["X-Amz-Signature"])
assert params["X-Amz-SignedHeaders"] == "content-length;content-type;host;x-amz-acl"

assert params["X-Amz-SignedHeaders"] ==
"cache-control;content-disposition;content-length;content-type;host;x-amz-acl"

assert headers == %{
"cache-control" => "public, max-age=31536000, immutable",
"content-disposition" => "inline",
"content-length" => "12345",
"content-type" => "video/mp4",
"x-amz-acl" => "public-read"
Expand Down
11 changes: 8 additions & 3 deletions test/mave_core_web/controllers/api/playback_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ defmodule MaveCoreWeb.Api.PlaybackControllerTest do
end
end

def presigned_get_url(bucket, path, _region, opts),
do: {:ok, "https://storage.example.test/#{bucket}/#{path}?expires=#{opts[:expires]}"}
def presigned_get_url(bucket, path, _region, opts) do
query = URI.encode_query([{"expires", opts[:expires]} | Keyword.get(opts, :query_params, [])])
{:ok, "https://storage.example.test/#{bucket}/#{path}?#{query}"}
end

setup do
previous_origin = Application.get_env(:mave_core, :playback_origin)
Expand Down Expand Up @@ -237,7 +239,10 @@ defmodule MaveCoreWeb.Api.PlaybackControllerTest do
|> get(base <> "/v1/h264_hd.mp4")

assert conn.status == 302
assert hd(get_resp_header(conn, "location")) =~ "v1/h264_hd.mp4"
location = URI.parse(hd(get_resp_header(conn, "location")))
assert location.path =~ "v1/h264_hd.mp4"
query = URI.decode_query(location.query)
assert query["response-cache-control"] == "private, max-age=#{query["expires"]}"

{:ok, other} = Embeds.create_video_embed(context.space)
wrong = sign(context.read_key, %{"sub" => context.space.hash <> other.hash})
Expand Down
Loading