操作指南:上传(高级)

前言

可靠的资产上传是每个 C2C 集成的核心功能。 本指南提供了用于创建稳健、具有韧性且高效上传系统的高级技术和最佳做法,即使在充满挑战性的环境中也能表现出色。

前提条件

如果您还没有阅读,请在继续之前先查阅实施 C2C:设置指南。 您需要用到在身份验证和授权流程中获取的 access_token。 我们将继续使用基础上传指南中的同一个测试资产

高级资产参数

在 Frame.io 中创建资产时,您可以使用多个高级参数来自定义上传行为。 其中,offset 参数对于正确集成尤为重要。

Offset - 处理暂停的设备

提供准确的 offset 值至关重要。 此参数用于指定媒体文件的创建时间,并确保您的设备不会上传本不该分享的内容。 当设备在 Frame.io 中暂停时,表示用户希望暂停期间创建的媒体文件不被上传。 有关更多详情,请参阅我们关于暂停功能的指南

offset 参数的其他优势

offset 参数为在 Frame.io 中组织媒体文件提供了另一个显著优势。 当上传在较早日期拍摄的内容时(例如,用户在回放期间选择上周拍摄的一张照片),offset 参数能够确保该媒体文件出现在与其原始拍摄日期对应的文件夹中,而不是当前上传日期对应的文件夹中。 这种按时间顺序组织的机制,能够在 Frame.io 的项目结构中保持逻辑清晰的时间线。 如果没有 offset 参数,历史媒体文件会被错误地与今天的内容归类在一起,这可能会给剪辑师及其他协作者造成困惑。 您可以通过自己的界面为用户提供此方面的选择。 如果用户希望无论媒体文件何时拍摄,都按当前日期对所有上传内容进行归类,您只需省略 offset 参数即可,因为当未指定该参数时,其默认值为 0

我们的 API 设计使得您的设备无需跟踪暂停状态。 在上传文件时,您只需指明该文件是在多少秒之前创建的。 我们的服务器会将该信息与暂停窗口期进行比较,如果文件是在暂停期间创建的,则会拒绝上传。

为了演示此功能,请通过“C2C Connections”选项卡中的三点菜单暂停您的设备。

现在尝试上传一个资产:

${
>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",
> "filesize": 21136250,
> "offset": 0
> }
>__JSON__
>} | python -m json.tool
API 端点规范

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

您将收到以下错误:

1{
2 "code": 409,
3 "errors": [
4 {
5 "code": 409,
6 "detail": "The channel you're uploading from is currently paused.",
7 "status": 409,
8 "title": "Channel Paused"
9 }
10 ],
11 "message": "Channel Paused"
12}

如果您取消暂停设备并使用相同的请求重试,则会创建资产。

但是,如果该资产是在暂停窗口期内创建的,您需要设置 offset 参数以反映其实际创建时间:

${
>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",
> "filesize": 21136250,
> "offset": 60
> }
>__JSON__
>} | python -m json.tool

这告诉 Frame.io 该资产是在 60 秒前(暂停期间)创建的,从而正确触发通道已暂停错误。 准确的 offset 值至关重要,可防止违背用户意愿上传敏感内容,包括受保护的知识产权、敏感素材或其他受限制的材料。

偏移和重试

当重试一次失败的资产创建调用时,请记得更新 offset 值。 在较长的重试周期中,一个固定不变的 offset 值可能会偏离相关的暂停窗口期,从而可能导致本应被阻止的上传请求得以通过。

上传到指定通道

如果您的设备有多个通道,您可以指定要使用哪个通道:

${
>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",
> "filesize": 21136250,
> "offset": -10,
> "channel": 2
> }
>__JSON__
>} | python -m json.tool

如果未指定,默认通道为 0。 大多数集成无需更改此值。

请求自定义分片数量

默认情况下,Frame.io 的后端会将文件划分为大约 25 MB 大小的分片。 对于高拥堵的网络环境,您可能希望使用更小的分片。 您可以使用 parts 参数请求指定数量的分片:

${
>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",
> "filesize": 21136250,
> "offset": 0,
> "parts": 4
> }
>__JSON__
>} | python -m json.tool

该响应将包含四个上传 URL:

1{
2 ...
3 "upload_urls": [
4 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-01-path]",
5 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-02-path]",
6 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-03-path]",
7 "https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-04-path]"
8 ],
9 ...
10}

分片大小为:

Python
1math.ceiling(float(21136250) / float(4))
2# 5284063 bytes

最后一个分片将为 5,284,061 字节(计算方式:21136250 - 5284063 * 3)。 请求自定义分片数量时,请注意 AWS S3 分段上传限制

  • 每个分段大小必须至少为 5 MiB(5,242,880 字节),最后一个分段除外
  • 总分段数不得超过 10,000 个

如果您的请求违反了这些限制,您将收到 500:内部服务器错误

{
"code": 500,
"errors": [
{
"code": 500,
"detail": "There was a problem with your request",
"status": 500,
"title": "Something went wrong"
}
],
"message": "Something went wrong"
}

请务必确保您的自定义分段数量符合 S3 的要求。

高效上传

C2C 设备通常在充满挑战性的网络环境中运行,因此效率至关重要。 以下是一些最大化吞吐量的策略。

TCP 连接重用/连接池

建立加密连接需要大量的协商开销。 为了高效运行,在发出多次请求时可重复使用 TCP 连接。 大多数 HTTP 库都提供了客户端会话抽象,用于维护持久连接。

一个新的 HTTPS 连接的协商过程包括加密握手和证书验证。 通过重用连接,您只需承担一次此开销,而不是对每个请求都执行一次。

TCP 握手参考

有关 TLS 握手过程的技术详情,请参阅 Cloudflare 的说明

为了演示使用 curl 进行连接重用,首先按照基础上传指南中的描述在 Frame.io 中创建一个新资产。

接下来,将文件拆分为单独的分片进行测试:

$head -c 10568125 ~/Downloads/C2C_TEST_CLIP.mp4 > "C2C_TEST_CLIP-Chunk01"
$tail -c 10568125 ~/Downloads/C2C_TEST_CLIP.mp4 > "C2C_TEST_CLIP-Chunk02"

现在使用 curl 的 --next 参数,通过单个 TCP 连接上传两个分片:

$curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-1-path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk01 \
>--next -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-2-path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk02

将其与单独连接的方式进行比较:

$curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-1-path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk01 \
>&& curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-2-path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk02
重用分片 URL

您可以多次上传到同一个分片 URL,因此能在不同示例之间随意重用这些 URL。

在测试中,连接重用通常可将顺序上传的性能提高 15-20%。

并行上传

为了获得更高的吞吐量,可以同时上传多个分片:

$curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-1-path] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk01 \
>& \
>curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[part-2-path]\
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk02 \
>&

在带宽充足的情况下,并行上传的完成时间大约等于最慢的单个上传所需的时间。

为了实现最佳并行上传,一个实用的经验法则是:每个 CPU 核心对应 2 个并发上传。 超过这个比例可能会导致资源争用和收益递减。

并行上传速度

网络状况会显著影响并行上传的性能。 在某些环境中,顺序上传可能比并行上传表现更好。 高级实施可以监测吞吐量并动态调整并发上传数量。 请务必在实际生产环境中进行性能分析,而不是依赖示例时间。

结合两种方法

为了达到最高效率,可以将连接池与并行上传相结合。 创建多个进程,每个进程在其自己的上传序列中使用连接池:

$curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[asset01-chunk01] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk01 \
>--next -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[asset01-chunk02] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk02 \
>& \
>curl -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[asset02-chunk01] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk01 \
>--next -X PUT https://frameio-uploads-production.s3-accelerate.amazonaws.com/parts/[asset02-chunk02] \
> --include \
> --header 'content-type: video/mp4' \
> --header 'x-amz-acl: private' \
> --data-binary @C2C_TEST_CLIP-Chunk02 \
>&
HTTP 库功能

大多数 HTTP 库都提供了用于连接池和并行请求的抽象。 请尝试您所使用库的选项,以确定适合您环境的最佳配置。

跟踪上传进度

您的集成必须为用户提供基本的进度指示。 可以接受分片级别的粒度——对于一个包含三个分片的上传任务,随着每个分片完成,进度可能依次递增为:0% → 33% → 66% → 100%。

更细粒度的进度报告取决于您所使用的 HTTP 库的能力。 如果您需要关于实施更详细的进度跟踪的指导,请联系我们的团队。

可靠上传

为了实现稳健的错误处理,请查阅我们的错误指南。 以下各节假设您已实施了该指南中描述的错误处理策略。

创建一个可用于生产环境的上传程序,除了处理单个请求的错误之外,还需要考虑更多因素。

创建上传队列

在现实场景中,您的设备生成媒体文件的速度可能会超过其上传速度,或者可能遇到长时间的网络连接中断。 实施队列系统可以将媒体创建与上传管理分离。

考虑双队列架构:

  1. 一个媒体队列,用于在 Frame.io 中注册本地文件
  2. 一个分片队列,用于上传各个文件块

下面是一个简化的实施示例:

Python
1# Where we are going to queue new files.
2FILE_QUEUE = Queue()
3
4# Where we are going to queue new chunks.
5CHUNK_QUEUE = Queue()
6
7# The http session that will handle TCP
8# connection pooling for us.
9HTTP_SESSION = http.Session()
10
11def take_picture():
12 """Snaps a picture for the user."""
13
14 image = MY_DEVICE.capture()
15 file_path = MY_DEVICE.write_image(image)
16 FILE_QUEUE.add(file_path)
17
18def task_register_assets():
19 """
20 Pulls snapped pictures from the FILE_QUEUE, registers
21 with Frame.io, and adds the chunks to the CHUNK_QUEUE.
22 """
23 while True:
24 # Get the latest file added to the queue and register
25 # a C2C Asset for it.
26 new_file = FILE_QUEUE.get()
27 asset = c2c.crete_asset_for_file(HTTP_SESSION, new_file)
28
29 # Calculate the size for each chunk
30 chunk_size = c2c.calculate_chunk_size(asset, new_file)
31
32 # Create a message for each chunk with it's parameters
33 # and add it to the
34 # queue.
35 chunk_start = 0
36 for chunk_url in asset.upload_urls:
37 message = {
38 "file_path": new_file,
39 "chunk_url": chunk_url,
40 "chunk_start": chunk_start,
41 "chunk_size": chunk_size,
42 }
43
44 # Put the message in the queue and
45 CHUNK_QUEUE.put(message)
46 chunk_start += chunk_size
47
48def task_upload_chunk():
49 """Takes a chunks and uploads them."""
50
51 while True:
52 info = CHUNK_QUEUE.get()
53 c2c.upload_chunk(HTTP_SESSION, info)
54
55def launch_upload_tasks():
56 """Lauches our Frame.io upload tasks."""
57 # Create a list to hold all of our tasks.
58 tasks = list()
59
60 # Create one task for registering assets.
61 asset_task = run_task_in_thread(task_register_assets)
62 tasks.append(asset_task)
63
64 # Create 2 tasks per CPU core for uploading chunks.
65 for _ in range(0, GET_CPU_COUNT() * 2):
66 chunk_task = run_task_in_thread(task_upload_chunk)
67 tasks.append(chunk_task)
68
69 # Run these tasks until shutdown
70 run_forever(tasks)
错误处理

在上述示例中,我们假设为 C2C 调用而调用的函数正按照错误指南中讨论的方式处理错误。

跨电源周期的持久队列

内存中的队列方法在设备保持通电状态下运行良好,但如果在上传完成前断电会怎样? 为了创建一个真正具有韧性的集成,我们需要确保设备在重启后能够从中断处恢复。

这需要在电源周期之间将队列状态持久保存到存储中。 像 SQLite 这样的嵌入式数据库为此功能提供了绝佳的基础。

您的持久队列实施应支持以下关键操作:

  • 将新创建的文件添加到上传队列
  • 跟踪资产在 Frame.io 中成功创建的时间
  • 记录资产因错误而创建失败的情况
  • 存储上传任务的文件分片信息
  • 检索下一个要上传的分片
  • 将分片标记为已成功上传
  • 记录分片上传失败
  • 提供文件状态信息以向用户展示

以下是我们如何调整之前的示例以使用持久存储系统:

Python
1# Our persistence layer for queuing uploads, potentially using SQLite
2# or another embedded database
3C2C_UPLOAD_STORE = NewC2CUploadStore()
4
5# HTTP session for connection pooling
6HTTP_SESSION = http.Session()
7
8def take_picture():
9 """Captures an image and adds it to the upload queue."""
10 image = MY_DEVICE.capture()
11 file_path = MY_DEVICE.write_image(image)
12
13 # Register the file with our persistent store
14 C2C_UPLOAD_STORE.add_file(file_path)
15
16def task_register_assets():
17 """
18 Processes files from persistent storage and registers
19 them with Frame.io for upload.
20 """
21 while True:
22 # Get the next available file from our store
23 file_record = C2C_UPLOAD_STORE.get_file()
24
25 try:
26 # Register the asset with Frame.io
27 asset = c2c.create_asset_for_file(HTTP_SESSION, file_record)
28 chunk_size = c2c.calculate_chunk_size(asset, file_record)
29
30 # Create entries for each chunk in our persistent store
31 chunk_start = 0
32 for chunk_url in asset.upload_urls:
33 message = {
34 "file_path": file_record,
35 "chunk_url": chunk_url,
36 "chunk_start": chunk_start,
37 "chunk_size": chunk_size,
38 }
39
40 C2C_UPLOAD_STORE.new_chunk(message)
41 chunk_start += chunk_size
42
43 except BaseException as error:
44 # Record the error in our persistent store
45 C2C_UPLOAD_STORE.file_asset_create_error(file_record, error)
46 else:
47 # Mark the asset as successfully created
48 C2C_UPLOAD_STORE.file_asset_created(file_record)
49
50def task_upload_chunk():
51 """Uploads individual file chunks from the persistent queue."""
52 while True:
53 # Get the next chunk, marking it as "in progress" to prevent
54 # other tasks from processing it simultaneously
55 chunk_record = C2C_UPLOAD_STORE.get_chunk()
56
57 try:
58 c2c.upload_chunk(HTTP_SESSION, chunk_record)
59 except BaseException as error:
60 # Record the error for potential retry
61 C2C_UPLOAD_STORE.chunk_error(chunk_record, error)
62 else:
63 # Mark successful completion
64 C2C_UPLOAD_STORE.chunk_success(chunk_record)
65
66def launch_upload_tasks():
67 """Launches Frame.io upload processing tasks."""
68 tasks = []
69
70 # Asset registration task
71 asset_task = run_task_in_thread(task_register_assets)
72 tasks.append(asset_task)
73
74 # Multiple parallel chunk upload tasks
75 worker_count = GET_CPU_COUNT() * 2
76 for _ in range(worker_count):
77 chunk_task = run_task_in_thread(task_upload_chunk)
78 tasks.append(chunk_task)
79
80 # Run indefinitely
81 run_forever(tasks)

采用这种持久存储方法后,您的集成将能够抵御电源中断的影响。 当设备重新启动时,只需从其上次保存的状态继续处理。 此架构也为实施更高级的功能(如错误跟踪和检测上传停滞)奠定了基础。

跟踪上传错误

一个稳健的上传系统必须仔细跟踪错误。 使用错误指南中的策略重试某个操作后,请将这些失败记录到您的持久化存储中。 这样,您的系统就可以:

  1. 降低有问题上传的优先级,防止它们阻塞整个队列
  2. 向用户提供准确的状态信息
  3. 针对持续存在的问题启用管理干预

当发生致命错误时,请标记该项,以避免不必要的重试尝试。

管理停滞的上传

实施防护措施,防止上传无限期停滞。 设置最长持续时间(例如 30 分钟),超过该时间后应终止并重新启动分片上传任务。 这样可以防止所有上传工作进程都被无响应操作阻塞的情况发生。

从静默故障中恢复

系统崩溃、断电或进程终止可能会阻止正常的错误报告。 从队列中检索项目时,请记录检出时间。 如果某个项目在超过合理阈值(例如 30 分钟)后仍处于“正在处理”状态,且未报告成功或失败,请自动将其返回到可用池中,以便由另一个工作进程处理。

缓解有毒上传

由于数据或环境本身的问题,“有毒”队列项目会持续失败。 如果这些项目不断重新排队,它们实际上可能会阻塞整个上传系统。 请考虑使用以下策略来处理此类情况:

  • 经过多次失败后,降低项目的优先级,以便可以继续处理较新的内容
  • 同时追踪显式错误和处理尝试次数
  • 遵循连接和授权的最佳做法,以区分临时环境问题与文件本身的内在问题
  • 实施递进式重试限制(例如,每个作业尝试内对单个操作重试 10 次,共尝试 3 个作业,总计 30 次尝试)
  • 提供一个用户界面,以便在环境问题解决后手动重置有问题的上传

“有毒”上传可能由以下原因导致:

  • 损坏的文件数据导致 I/O 错误
  • 灾难性进程故障阻止错误报告
  • 原本可重试的错误因永久性的潜在条件而被触发

系统重启后重试

在永久放弃有问题的上传之前,标记它们,以便在下次系统重启后进行最后一次重试。 这可以应对因内存、驱动程序或资源分配等临时系统状态问题而导致上传失败的情况。 如果某个上传在彻底重启后仍然失败,则可以更有把握地将其标记为永久性问题。

清空您的队列

请记得从队列中移除不可用的文件。 当媒体被物理移除或文件被删除时,请从上传队列中清除相应条目,以防止不必要的错误。

重要的是,连接到新项目时必须清空上传队列。 为一个项目排队的媒体绝不应出现在另一个项目中。 当用户将设备与不同项目进行配对时,请验证项目是否已更改;如果是,请完全清空现有队列。

后续步骤

我们鼓励您就任何疑问联系我们的团队,并继续参阅高级上传指南。 我们期待为您的集成进度提供支持。