操作指南:上传(实时)

前言

在我们掌握了基本的上传知识后,让我们进一步探讨如何在资产创建过程中进行实时上传。 这种方法支持在录制、渲染或流式传输过程中上传文件,无需等待确定最终文件大小。

通过实时上传 API,资产在录制完成后仅需数秒即可在 Frame.io 中播放,显著提升了工作流程效率。

演示视频

如需快速预览此功能,请观看我们的视频演示。 该演示展示了从 Adobe Media Encoder 实时上传渲染文件的过程,视频在渲染完成后仅需 5 秒即可在 Frame.io 中播放。

前提条件

如果您尚未阅读,请先参阅实施 C2C:设置指南。 您需要用到在身份验证和授权流程中获取的 access_token。 在示例中,我们将使用基础上传指南中的同一个测试资产。 建议先熟悉基础上传指南,因为我们将基于其中的概念进行扩展。

创建实时资产

实时上传始于修改后的资产创建流程。 创建资产时,需将 is_realtime_upload 设置为 true,并省略 filesize 参数(或将其设置为 null),因为在创建过程中无法确定最终大小:

${
>curl -X POST https://api.frame.io/v2/devices/assets \
> --header 'Authorization: Bearer [access_token]' \
> --header 'Content-Type: application/json' \
> --header 'x-client-version: 2.0.0' \
> --data-binary @- <<'__JSON__'
> {
> "name": "C2C_TEST_CLIP.mp4",
> "filetype": "video/mp4",
> "is_realtime_upload": true
> }
>__JSON__
>} | python -m json.tool
API 端点规范

/v2/devices/assets 的文档可以在此处找到

扩展名和文件名

实时资产需要提供文件扩展名。 如果在创建资产时不知道文件名,您可以使用 extension 字段代替(格式:'.mp4')。 如果您计划稍后更新资产名称,建议采用此方式。

与标准资产创建相比,实时资产的响应得到了简化:

1{
2 "id": "{asset_id}",
3 "name": "C2C_TEST_CLIP.mp4"
4}

请注意,结果中不包含 upload_urls——对于实时上传,我们将在创建文件时按需生成上传 URL。

请求上传 URL

接下来,我们将使用上一个响应中的 asset_id,为文件的前半部分(10,568,125 字节)请求一个 URL:

${
>curl -X POST https://api.frame.io/v2/devices/assets/{asset_id}/realtime_upload/parts \
> --header 'Authorization: Bearer [access_token]' \
> --header 'Content-Type: application/json' \
> --header 'x-client-version: 2.0.0' \
> --data-binary @- <<'__JSON__'
> {
> "parts": [
> {
> "number": 1,
> "size": 10568125,
> "is_final": false
> }
> ]
> }
>__JSON__
>} | python -m json.tool
API 端点规范

/v2/devices/assets/{asset_id}/realtime_upload/parts 的文档可以在此处找到

理解请求参数:

  • parts:我们需要获取 URL 的上传分段列表。 在单次调用中请求多个 URL 可提高效率。

  • number:分段的顺序编号,从 1 开始。 编号可以跳过,且分段可以按任意顺序上传,但会按顺序进行组装。 不能超过 10,000(AWS 限制)。 * size:分段大小(以字节为单位)。 必须符合 AWS 多分段上传限制。 * is_final:指示是否为文件的最后一个分段。

响应包含所请求的上传 URL:

1{
2 "upload_urls": [
3 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part_01_path]"
4 ]
5}

upload_urls 列表直接对应于 parts 请求顺序。

现在按照基础上传指南中的方式上传第一个分片:

$head -c 10568125 ~/Downloads/C2C_TEST_CLIP.mp4 | \
>curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part_01_path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @-

接下来,为第二个也是最后一个分段请求 URL:

${
>curl -X POST https://api.frame.io/v2/devices/assets/{asset_id}/realtime_upload/parts \
> --header 'Authorization: Bearer [access_token]' \
> --header 'Content-Type: application/json' \
> --header 'x-client-version: 2.0.0' \
> --data-binary @- <<'__JSON__'
> {
> "asset_filesize": 21136250,
> "parts": [
> {
> "number": 2,
> "size": 10568125,
> "is_final": true
> }
> ]
> }
>__JSON__
>} | python -m json.tool

请注意以下重要的新增内容:

  • 对于最后一个分段,将 is_final 设置为 true,表示上传将在该分片完成后结束
  • asset_filesize 提供文件的总大小,当任何分段设置 is_final: true 时,此参数均为必填项

在响应中收到 URL 后:

1{
2 "upload_urls": [
3 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part_02_path]"
4 ]
5}

上传最后一个分片:

$tail -c 10568125 ~/Downloads/C2C_TEST_CLIP.mp4 | \
>curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part_02_path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @-
最终分段处理

当最后一个分段上传完成后,Frame.io 开始组装完整文件。 此过程包括一个 60 秒的宽限期,用于等待其余分段完成上传。 我们建议仅在所有其他分段成功上传之后,再上传最后一个分段。

就是这样! 导航到 Frame.io 即可查看您成功上传的实时资产。 🎉

管理资产名称

如果在创建资产时文件名未知,您可以使用扩展名字段而不指定名称

${
>curl -X POST https://api.frame.io/v2/devices/assets \
> --header 'Authorization: Bearer [access_token]' \
> --header 'Content-Type: application/json' \
> --header 'x-client-version: 2.0.0' \
> --data-binary @- <<'__JSON__'
> {
> "extension": ".mp4",
> "filetype": "video/mp4",
> "is_realtime_upload": true
> }
>__JSON__
>} | python -m json.tool

系统将分配一个默认名称:

1{
2 "id": "{asset_id}",
3 "name": "[new file].mp4"
4}

您可以在请求上传 URL 时包含 asset_name 字段来更新此名称:

${
>curl -X POST https://api.frame.io/v2/devices/assets/{asset_id}/realtime_upload/parts \
> --header 'Authorization: Bearer [access_token]' \
> --header 'Content-Type: application/json' \
> --header 'x-client-version: 2.0.0' \
> --data-binary @- <<'__JSON__'
> {
> "asset_name": "C2C_TEST_CLIP.mp4",
> "asset_filesize": 21136250,
> "parts": [
> {
> "number": 2,
> "size": 10568125,
> "is_final": true
> }
> ]
> }
>__JSON__
>} | python -m json.tool

仅当资产仍为其默认名称时,该名称才会更新;如果资产已在 Frame.io UI 中重命名或之前已更新,则请求将被忽略。

优化 URL 请求

为了提高效率,请根据当前可用的数据量,请求尽可能多的分段 URL,而不是逐个请求。 这种方法对于上传速度可能落后于数据生成速度的大文件尤其有价值。

处理媒体文件标头

某些媒体格式需要在文件开头包含标头,而这些标头只有在整个文件完成后才会写入。 当标头大小低于 AWS 最小分段大小 5 MiB(5,242,880 字节)时,就会带来问题。

我们的建议如下:

  1. 保留媒体数据的前 5,242,880 字节,暂不上传
  2. part_number=2 开始上传后续分段
  3. 文件完成后,将标头添加到所保留数据的前面
  4. part_number=1 请求一个 URL,并上传此合并后的分片

这种方法既能确保您的第一个分片满足最小大小要求,又能保持正确的文件结构。

调整分段大小以获得最佳性能

AWS 对上传策略施加了以下限制:

  • 最大文件大小:5 TiB(5,497,558,138,880 字节)
  • 最大分段数量:10,000
  • 最小分段大小:5 MiB(5,242,880 字节)

固定分段大小会产生以下权衡:

  • 若所有 10,000 个分段都使用最小大小 (5 MiB),则总文件大小上限约为 52.4 GB
  • 若要平均分配最大文件大小,则需要约 550 MB 分片,这对于较小文件的高效流传输来说又太大了

我们需要一个能在这些约束之间取得平衡的公式:从较小的分段开始以实现响应式上传,同时确保在需要时能够处理非常大的文件。

推荐的分段大小公式

以下是我们建议的 Python 方法:

Python
1import math
2from typing import Callable
3
4# Constants
5MINIMUM_PART_SIZE = 5_242_880
6MAXIMUM_PART_COUNT = 10_000
7MAXIMUM_FILE_SIZE = 5_497_558_138_880
8
9# Maximum uniform data rate that allows for 10,000 parts
10MAXIMUM_DATA_RATE = MAXIMUM_FILE_SIZE // MAXIMUM_PART_COUNT
11
12def part_size(part_number: int, format_bytes_per_second: int) -> int:
13 """
14 Returns the payload size for a specific part number given the file's
15 expected data rate.
16 """
17 if part_number < 1:
18 raise ValueError("part_number must be greater than 0")
19
20 if part_number > 10_000:
21 raise ValueError("part_number must be less than 10,000")
22
23 # Make sure we never go above the maximum data rate or fall below the
24 # minimum part size, even if the data rate is lower.
25 data_rate = min(format_bytes_per_second, MAXIMUM_DATA_RATE)
26 data_rate = max(data_rate, MINIMUM_PART_SIZE)
27
28 # Calculate a scalar given our data rate. We will explain this step
29 # futher on in the guides.
30 scalar = -(2 * (125 * data_rate - 68_719_476_736)) / 8_334_583_375
31 part_size = math.floor(scalar * pow(part_number, 2)) + data_rate
32
33 return part_size

…其中 part_number 介于 110_000 之间(包含边界值),format_bytes_per_second 是您的文件预计每秒消耗的平均字节数。 我们将在后面详细介绍该公式的推导过程。

标量值

标量变量及其计算初看可能有些费解,但它是一个数学工具,可以确保:无论我们为 format_bytes_per_second 取何值,只要将 110_0000 之间所有允许的 part_number 值输入到函数中,我们都会得到一组值,其总和 恰好 等于 5 TiB 的文件大小限制——或者说尽可能精确。 我们将在后面展示该公式的推导过程

向下取整

通过使用向下取整,我们会舍弃少量字节,但可确保对 10,000 个分段进行常规四舍五入时不会意外导致超出允许的最大文件大小。 通过这种方式,最多只会舍弃 10,000 个字节(即 10 KB),这是一个可以接受的权衡。

此公式的重要特性如下:

  • 当上传 10,000 个分段时,上传的数据总量与 5 TiB 文件大小上限的差距在 10 KB 以内。
  • 在开始阶段针对较小、更高效的负载进行优化,以提高中短时长片段的响应速度。
  • 对于超长片段,从文件写入完成到可在 Frame 中播放之间的响应速度会有所降低。

第二点与第三点之间的权衡之所以可以接受,是因为大多数片段不会达到触发第三点的大小。 我们是以极少数文件的响应速度降低为代价,换取了 绝大多数 文件响应速度的提升。

一个更先进、更高效的公式版本(该版本会生成一个匿名函数 part_size_calculator,其中预先计算并内嵌了我们的静态标量和数据速率)可能如下所示:

Python
1def create_part_size_calculator(format_bytes_per_second: int) -> Callable[[int], int]:
2 """
3 Returns a function that takes in a `part_number` and returns a
4 `part_size` based on `data_rate`.
5 """
6
7 # Make sure we never go above the maximum data rate or fall below the
8 # minimum part size, even if the data rate is lower.
9 data_rate = min(format_bytes_per_second, MAXIMUM_DATA_RATE)
10 data_rate = max(data_rate, MINIMUM_PART_SIZE)
11
12 static_scalar = -(2 * (125 * data_rate - 68_719_476_736)) / 8_334_583_375
13
14 def part_size_calculator(part_number: int) -> int:
15 """Calculates size in bytes of upload for `part_number`."""
16 if part_number < 1:
17 raise ValueError("part_number must be greater than 0")
18
19 if part_number > 10_000:
20 raise ValueError("part_number must be less than 10,000")
21
22 return math.floor(static_scalar * pow(part_number, 2) + data_rate)
23
24 return part_size_calculator

公式的表现情况。

我们来看看上述公式在几种常见文件类型上的输出特征。

示例 1:Web 格式

对于数据速率约为 5.3MB/s 或更低(大多数 H.264/H.265/HEVC 文件)的可网页播放格式,我们将获得如下所示的负载大小递进序列:

总分段数负载字节数 (Bytes)负载兆字节数 (MB)文件总字节数 (Bytes)文件总吉字节数 (GB)
15,242,8965.2 MB5,242,8960.0 GB
1,00021,575,81721.6 MB10,695,361,35710.7 GB
5,000413,566,329413.6 MB706,957,655,928707.0 GB
10,0001,638,536,6791,638.5 MB5,497,558,133,9215,497.6 GB
表格列键 - 总分段数:上传到 AWS 的文件分段总数。 - 负载字节数 (Bytes):当 part_number 等于总分段数时,AWS PUT 负载的大小。 - 负载兆字节数 (MB):与负载字节数相同,但以兆字节 (MB) 为单位。 - 文件总字节数 (Bytes):当按顺序上传完总分段数个分段后,文件已上传的总字节数。 - 文件总吉字节数 (GB):与文件总字节数相同,但以吉字节 (GB) 为单位。

这些数值在实际上传中取得了很好的平衡,尤其是对于 H.264 等 Web 播放编解码器;大多数文件都将小于 10.7 GB,因此能在 1,000 个分段内完成上传。 负载大小永远不会超过 21.6 MB。

即使我们处理了一半的分段,负载大小仍然不会超过 413.5 MB。 此时上传总量将达到 707 GB,对于绝大多数 Web 文件来说已经绰绰有余。

只有当我们接近允许的分段数量上限时,文件大小才会开始急剧增大。 但是,它永远不会超过 1.7 GB,远低于 AWS 规定的每个分段 5 GiB 的限制。

示例 2:Prores 422 LT Prores 422 LT 的数据速率为 102 Mbps,生成如下表格:

总分段数负载字节数 (Bytes)负载兆字节数 (MB)文件总字节数 (Bytes)文件总吉字节数 (GB)
112,750,01612.8 MB12,750,0160.0 GB
1,00028,857,75828.9 MB18,127,308,78318.1 GB
5,000415,443,954415.4 MB735,107,948,432735.1 GB
10,0001,623,525,8171,623.5 MB5,497,558,133,9585,497.6 GB

此表格展示了与我们针对 Web 优化后的公式相比的一些有用特性。 在前 1,000 个分段中,我们能够多上传 8 GB 的文件。 较大的初始负载意味着我们在开始时不需要过快地请求 URL,从而使上传在更高数据速率下更加高效。 在上传过程的末尾阶段,我们的负载大小仍然保持在较大水平。

示例 2:Camera Raw

最后,让我们尝试一种数据速率为 280 MB/s 的相机 RAW 格式。 面对如此高速的数据输入,如果一开始还尝试以 5 MiB 大小的分片进行上传,那就完全没有意义了:

总分段数负载字节数 (Bytes)负载兆字节数 (MB)文件总字节数 (Bytes)文件总吉字节数 (GB)
1280,000,008280.0 MB280,000,0080.3 GB
1,000288,091,460288.1 MB282,701,200,139282.7 GB
5,000482,286,516482.3 MB1,737,245,341,5421,737.2 GB
10,0001,089,146,0651,089.1 MB5,497,558,133,8705,497.6 GB

不仅早期的负载更高效,而且我们在上端节省了超过半吉字节的数据,这将使那些网络调用更不易受到不良网络事件的影响。

展示我们的推导过程

在我们将所有内容整合到一个示例上传程序之前,让我们先看看我们是如何得出这个公式的。

我们需要做的是设计一个公式,用允许分段数量末尾的大体积重型负载(大多数上传永远不会达到这个阶段)来换取靠近开头部分的轻量高效负载,这是每一次上传都能受益的一点。 与此同时,我们还要确保我们的算法能够 恰好 在第 10,000 个分段时,接近 5 TiB 的文件大小限制。

是时候运用一些微积分了。

我们希望曲线呈指数级增长,因此公式应该类似于:

n2n^2

…其中 n 是分段编号。 我们还希望确保每个分段至少是我们公式中的数据速率,我们将其称为 r

n2+rn^2 + r

现在我们需要找到一个公式,可以计算 这个 公式在前 10,000 个自然数(1、2、3…)上的总和。 Sigma Σ 符号表示求和。 我们把它添加到公式中:

Σxn2+rΣxn^2 + r

…并将 n 重新定义为 1 到 10,000(含边界值)之间的自然数序列。 这个方程式对我们来说还不是很有用。 它具有正确的直观形状,但如果像我们希望的那样代入 n=10,000r=5,242,880,它只会输出一个结果385,812,135,000 (385 GB)。 这个结果不仅远低于我们 5 TiB 的文件大小限制,而且也没有办法调整这个公式使其输出我们想要的结果。

让我们给自己加一个可以调节的旋钮:

Σxn2+rΣxn^2 + r

…其中 x 是一个标量,我们可以通过求解它来使得结果为 5 TiB。 现在我们可以将方程式设置为等于我们的文件大小限制,然后求解 x

Σxn2+r=5,497,558,138,880Σxn^2 + r = 5,497,558,138,880

通常,求和必须通过迭代(如在 forwhile 循环中)来求解。 但事实证明,有一个完美的公式适合我们:一种已知的方法,能够以低成本计算前 n 个自然数的平方和:

Σn2=n(n+1)(2n+1)/6Σn^2 = n(n+1)(2n+1)/6

将其重新排列为多项式,使其更易于查看:

Σn2=(2n3+3n2+n)/6Σn^2 = (2n^3 + 3n^2 + n)/6

我们可以将变量 xr 添加到等式两边:

Σxn2+r=x(2n3+3n2+n)/6+rnΣxn^2 + r = x(2n^3 + 3n^2 + n)/6 + rn

最后,我们将新公式设为等于 5 TiB:

x(2n3+3n2+n)/6+rn=5,497,558,138,880x(2n^3 + 3n^2 + n)/6 + rn = 5,497,558,138,880

现在,我们需要做的就是通过代入 n=10,000(我们的总分段数)来求解 x。 这将为我们提供一种针对给定数据速率计算静态标量的方法。 让我们把它交给 Wolfram Alpha 来求解,而不是手动计算:

x=(2(125r68719476736))/8334583375x = -(2 (125 r - 68719476736)) / 8334583375

现在我们有所进展了! 如果我们的数据速率为最小分段大小 (5 MiB),我们会得到一个静态标量:

136,128,233,472/8,334,583,375136,128,233,472 / 8,334,583,375

在计算机领域,这表示一个 float64 值:16.33293799427617。 在这种情况下,我们确定分段大小的公式为:

s=16.33293799427617n2+5,242,880s = 16.33293799427617n^2 + 5,242,880

其中 s 是我们的分段大小。

我们还有一个问题。 在现实世界中,我们不能使用非整数字节的负载。 我们需要对每个值进行取整。 我们将使用 Python,并向下取整:

Python
1math.floor(16.33293799427617 * pow(part_number, 2)) + 5_242_880

至此,我们得到了本指南所给原始函数的一个具体示例。

构建一个基础上传程序

让我们来看一些简单的类似 Python 的伪代码,用于实时上传正在渲染的文件,其中用到了我们在本指南中学到的所有内容:

Python
1import math
2from datetime import datetime, timezone
3from typing import Callable
4
5# The minimum size, in bytes, for a single, non-final part upload.
6MINIMUM_PART_SIZE = 5_242_880
7# The maximum filesize in
8MAXIMUM_PART_COUNT = 10_000
9# The maximum size, in bytes, for an AWS upload.
10MAXIMUM_FILE_SIZE = 5_497_558_138_880
11
12# The data rate at which every part is an equal size, and could not
13# be any uniformly larger without violating the maximum total file
14# size if 10_000 parts were to be uploaded. it works out to
15# ~549.8 MB per payload. By enforcing this we actually never need
16# to check if a part exceeds the maximum allowed part size, as our
17# parts will never exceed ~549.8 MB.
18MAXIMUM_DATA_RATE = MAXIMUM_FILE_SIZE // MAXIMUM_PART_COUNT
19
20def create_part_size_calculator(format_bytes_per_second: int) -> Callable[[int], int]:
21 """
22 Returns a function that takes in a `part_number` and returns a
23 `part_size` based on `data_rate`.
24 """
25 ...
26
27def upload_render(data_stream: DataStream, channel: int = 0) -> None:
28 """
29 Uploads an asset for data_stream, which is a custom IO class that pulls remaining
30 upload data from an internal buffer or file, depending on how well the upload is
31 keeping pace with the render.
32
33 Uploads to `channel`
34 """
35
36 asset = c2c.asset_create(
37 extension=data_stream.extension,
38 filetype=data_stream.mimetpye,
39 channel=channel,
40 offset=datetime.now(timezone.utc) - data_stream.created_at()
41 )
42
43 calculate_part_size = create_part_size_calculator(data_stream.data_rate())
44 next_part_number = 0
45
46 while True:
47 next_payload_size = calculate_part_size(next_part_number)
48
49 # Waits until one or more chunks worth of data is ready for upload. Cache
50 # whether our data stream has completed writing the file, and the current
51 # number of bytes we have remaining to upload at this time.
52 available_bytes, stream_complete = data_stream.wait_for_available_data(
53 minimum_bytes=next_payload_size
54 )
55
56 # Build the list of parts to request based on our available data.
57 parts = []
58 while available_bytes > 0:
59 payload_size = calculate_part_size(next_part_number)
60
61 if available_bytes < payload_size and not stream_complete:
62 break
63
64 payload_size = min(payload_size, available_bytes)
65
66 parts.append(
67 c2c.RealtimeUploadPart(
68 part_number=next_part_number,
69 part_size=payload_size,
70 is_final=False
71 )
72 )
73
74 available_bytes -= payload_size
75 next_part_number += 1
76
77 # If our stream is done writing, mark the last part as final.
78 if stream_complete:
79 parts[-1].is_final = True
80
81 # Create the part URLs using the C2C endpoint.
82 response = c2c.create_realtime_parts(
83 asset_id=asset.id,
84 asset_name=None if not stream_complete else data_stream.filename,
85 asset_filesize=None if not stream_complete else data_stream.size(),
86 parts=parts
87 )
88
89 # Upload each part to its URL.
90 for part, part_url in zip(parts, response.upload_urls):
91 part_data = data_stream.read(bytes=part.size)
92 c2c.upload_chunk(part_data, part_url, data_stream.mimetype)
93
94 if stream_complete:
95 break
高级上传

上面的代码仅演示了实时上传文件的基本流程。 实际上,此逻辑需要通过错误处理高级上传技术进行增强。

下一步

实时上传提供了一种方法,让您的集成尽可能响应迅速,素材在录制完成后几秒钟内便可在 Frame.io 中播放。 后续指南将涵盖高级上传技术和相关要求。 尽管本指南主要针对基础上传而编写,但其中大部分内容仍适用于实时上传。

如果您还没有联系我们的团队,我们鼓励您这样做,然后继续阅读下一份指南。 我们期待收到您的回复!