تخطَّ إلى المحتوى

الرفع المباشر

يرسل تطبيقك ملف الفيديو مباشرة إلى التخزين. وخادمك لا ينقل الملف. والرفع 4 خطوات:

  1. الإنشاء (Create). تحصل على رابط موقّع واحد لكل جزء.
  2. الرفع (PUT). ارفع كل جزء إلى رابطه. واحفظ ترويسة ETag من كل رد.
  3. الإكمال (Complete). أكمل الرفع بقائمة أرقام الأجزاء وقيم ETag.
  4. نعالج الفيديو. استعلم عن حالة الفيديو حتى تصبح ready (يأتي إشعار video.ready مع إصدار المعالجة).

الروابط الموقّعة للرفع فقط. أمّا التشغيل فيستخدم رموز تشغيل موقّعة منفصلة. ويأتي دليلها عند إطلاق التشغيل الموقّع.

  • معرّف مشروع (prj_...) ومفتاح API لذلك المشروع (sk_...).
  • عنوان الأساس في TEST: https://test-api.segmnt.cloud
  • أرسل المفتاح في ترويسة Authorization: Bearer <key> في نداءات API. ولا ترسله إلى روابط الأجزاء.
البند القيمة
أنواع الملفات video/mp4, video/quicktime, video/webm, video/x-matroska
الحدّ الأقصى للحجم حسب خطتك. والافتراضي 5 جيبيبايت حتى تصبح الخطط جاهزة.
حجم الجزء partSizeBytes من الرد. ولا يقلّ عن 100 مبيبايت.
أقصى عدد أجزاء 10,000 (يزيد حجم الجزء للملفات الكبيرة جداً)
الجزء الأخير يمكن أن يكون أصغر. أمّا بقية الأجزاء فيجب أن تساوي partSizeBytes تماماً.
عمر الرابط حتى expiresAt (24 ساعة بعد البداية)
العنوان من 1 إلى 200 حرف
اسم الملف من 1 إلى 255 حرف

الملفات الصغيرة (أقل من 100 مبيبايت) لها جزء واحد.

يُفحص حدّ الحجم 3 مرّات: عند إنشاء الرفع (قيمة sizeBytes المعلنة)، وعند الإكمال (الحجم الحقيقي)، وقبل المعالجة (المدّة الحقيقية). وإذا تجاوز الملف الحقيقي الحدّ، تنتهي حالة الفيديو بـ failed مع failure.code = plan_limit_reached، ويُحذف الملف. وإذا كان الحجم الحقيقي أكبر من sizeBytes الذي أعلنته، ينتهي الفيديو بـ failed مع size_mismatch.

نافذة طرفيّة
curl -X POST "$BASE/v1/projects/$PROJECT/uploads" \
-H "Authorization: Bearer $SEGMNT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Lesson 1","fileName":"lesson-01.mp4","sizeBytes":2147483648,"contentType":"video/mp4"}'

الرد (201):

{
"id": "upl_2bN6rD",
"video": { "id": "vid_9xR4tL", "status": "uploading", "...": "..." },
"partSizeBytes": 104857600,
"parts": [
{ "partNumber": 1, "url": "https://..." },
{ "partNumber": 2, "url": "https://..." }
],
"expiresAt": "2026-10-12T07:00:00Z"
}

Idempotency-Key اختياري، لكننا نوصي به. فإذا انتهت مهلة طلبك، أعد إرساله بالمفتاح نفسه (خلال 24 ساعة) فتحصل على الرفع نفسه.

تعامل مع روابط الأجزاء كأنها أسرار. ولا تسجّلها في السجلات.

الخطوة 2: ارفع الأجزاء (PUT)

Section titled “الخطوة 2: ارفع الأجزاء (PUT)”

اقطع الملف إلى قطع بحجم partSizeBytes. ثم أرسل PUT بالبايتات الخام للقطعة رقم N إلى رابط partNumber N. ولا حاجة لأي ترويسة أخرى.

اقرأ ترويسة ETag من الرد واحفظها مع رقم الجزء. ويمكن للمتصفحات قراءة ETag لأن تخزيننا يظهرها في CORS.

ويمكنك إرسال الأجزاء بالتوازي (4 في الوقت نفسه بدايةً جيدة) وبأي ترتيب.

نافذة طرفيّة
curl -X POST "$BASE/v1/projects/$PROJECT/uploads/$UPLOAD/complete" \
-H "Authorization: Bearer $SEGMNT_KEY" \
-H "Content-Type: application/json" \
-d '{"parts":[{"partNumber":1,"etag":"\"abc...\""},{"partNumber":2,"etag":"\"def...\""}]}'

أرسل كل الأجزاء. واستخدم قيمة ETag كما استلمتها بالضبط (أبقِ علامات الاقتباس) — فنقبل ETag بعلامات اقتباس أو بدونها. ويجب أن تأتي ETag من رقم الجزء نفسه. وإذا أُرسل جزء مرّتين، فاستخدم ETag من أحدث رد. الرد: 202 مع الفيديو، وحالته الآن processing.

الإكمال آمن للتكرار. فإذا كان الرفع مكتملاً بالفعل، تحصل على 202 مع الفيديو الحالي.

تنتقل حالة الفيديو إلى processing ثم ready (أو failed). استعلم عبر GET /v1/projects/{projectId}/videos/{videoId} حتى تتغيّر الحالة. وإشعار video.ready ليس مباشراً بعد؛ فهو يُطلق مع إصدار المعالجة، وسيظهره هذا الدليل حينها. وفي حالات نادرة قد يحتفظ فريقنا بالفيديو للمراجعة (الحالة held). ولا يُشغَّل حتى ينتهي الاحتفاظ.

نافذة طرفيّة
curl -X DELETE "$BASE/v1/projects/$PROJECT/uploads/$UPLOAD" \
-H "Authorization: Bearer $SEGMNT_KEY"

الرد: 204. وتُحذف الأجزاء والفيديو. وإعادة النداء آمنة. وإذا كان الرفع مكتملاً بالفعل، تحصل على 409 upload_closed. والرفوعات غير المكتملة تُحذف بعد expiresAt.

الأخطاء تستخدم application/problem+json. استخدم الحقل code في الكود، لا النص.

الموضع الحالة code المعنى ما تفعله
create 400 invalid_request حقل ناقص أو خاطئ، أو نوع ملف غير مدعوم صحّح الطلب. لا تُعِد المحاولة.
any 401 unauthorized مفتاح ناقص أو خاطئ أو مُبطَل تحقّق من المفتاح.
any 405 method_not_allowed طريقة HTTP خاطئة للرابط تحقّق من الرابط وطريقة HTTP.
create 403 plan_limit_reached الملف أكبر مما تسمح به خطتك استخدم ملفاً أصغر أو غيّر الخطة.
any 404 not_found معرّف مشروع أو رفع خاطئ، أو المفتاح لمشروع آخر تحقّق من المعرّفات.
complete 400 invalid_request قائمة parts غير صحيحة صحّح الطلب.
complete 400 invalid_parts أرقام الأجزاء أو قيم ETag لا تطابق ما رُفع تأكّد أن كل جزء رُفع وأن قيم ETag صحيحة.
complete 409 upload_closed أُلغي الرفع أو انتهت صلاحيته ابدأ رفعاً جديداً.
cancel 409 upload_closed الرفع مكتمل بالفعل لا شيء لإلغائه.
create 429 (الكود غير محدّد في المواصفة بعد) طلبات كثيرة انتظر عدد ثواني Retry-After، ثم أعد المحاولة.

أخطاء PUT للأجزاء تأتي من التخزين، لا من واجهتنا. انظر القسم التالي.

النداء آمن للتكرار؟ الطريقة
Create نعم، مع Idempotency-Key المفتاح نفسه يعيد الرفع نفسه. وبدون مفتاح، ينشئ التكرار فيديو ثانياً.
PUT للجزء نعم أرسل الجزء نفسه مرّة أخرى. والأخير يفوز. استخدم أحدث ETag. وأعِد المحاولة عند أخطاء الشبكة و5xx و429 بانتظار متزايد (1 ثانية، 2 ثانية، 4 ثوانٍ).
PUT للجزء يعيد 403 لا انتهت صلاحية الرابط. ابدأ رفعاً جديداً.
Complete نعم النص نفسه. وإذا ضاع الرد، أرسله مرّة أخرى.
Cancel نعم دائماً 204 لرفع مُلغى.

لا تُعِد المحاولة عند 400 و401 و403 و404 من واجهتنا دون تغيير.

جُرّبت هذه الأمثلة على بيئة TEST. فكل من أمثلة Node وPython وPHP رفع ملفاً بحجم 250 مبيبايت (3 أجزاء) ووصل الفيديو إلى حالة processing. كما تغطّي اختباراتنا مسارات الإنشاء والإكمال والإلغاء والأخطاء.

import { open } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://test-api.segmnt.cloud";
const { SEGMNT_KEY, PROJECT_ID } = process.env;
const api = (path, init = {}) =>
fetch(`${BASE}/v1/projects/${PROJECT_ID}${path}`, {
...init,
headers: { Authorization: `Bearer ${SEGMNT_KEY}`, "Content-Type": "application/json", ...init.headers },
});
async function putPart(url, bytes) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch(url, { method: "PUT", body: bytes });
if (res.ok) return res.headers.get("etag");
if (res.status !== 429 && res.status < 500) throw new Error(`part failed: ${res.status}`);
} catch (e) {
if (String(e.message).startsWith("part failed")) throw e;
}
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
throw new Error("part failed after retries");
}
export async function upload(path, title) {
const file = await open(path);
const { size } = await file.stat();
const res = await api("/uploads", {
method: "POST",
headers: { "Idempotency-Key": randomUUID() },
body: JSON.stringify({ title, fileName: path.split("/").pop(), sizeBytes: size, contentType: "video/mp4" }),
});
if (!res.ok) throw new Error(`create failed: ${res.status} ${await res.text()}`);
const up = await res.json();
try {
const parts = [];
for (const { partNumber, url } of up.parts) {
const start = (partNumber - 1) * up.partSizeBytes;
const length = Math.min(up.partSizeBytes, size - start);
const buf = Buffer.alloc(length);
await file.read(buf, 0, length, start);
parts.push({ partNumber, etag: await putPart(url, buf) });
}
const done = await api(`/uploads/${up.id}/complete`, { method: "POST", body: JSON.stringify({ parts }) });
if (!done.ok) throw new Error(`complete failed: ${done.status} ${await done.text()}`);
return await done.json();
} catch (e) {
await api(`/uploads/${up.id}`, { method: "DELETE" });
throw e;
} finally {
await file.close();
}
}
import os, time, uuid, requests
BASE = "https://test-api.segmnt.cloud"
PROJECT = os.environ["PROJECT_ID"]
HEADERS = {"Authorization": f"Bearer {os.environ['SEGMNT_KEY']}"}
API = f"{BASE}/v1/projects/{PROJECT}"
def put_part(url, data):
for attempt in range(5):
try:
r = requests.put(url, data=data, timeout=300)
if r.ok:
return r.headers["ETag"]
if r.status_code != 429 and r.status_code < 500:
raise RuntimeError(f"part failed: {r.status_code}")
except requests.RequestException:
pass
time.sleep(2 ** attempt)
raise RuntimeError("part failed after retries")
def upload(path, title):
size = os.path.getsize(path)
r = requests.post(
f"{API}/uploads",
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={"title": title, "fileName": os.path.basename(path),
"sizeBytes": size, "contentType": "video/mp4"},
)
r.raise_for_status()
up = r.json()
try:
parts = []
with open(path, "rb") as f:
for p in up["parts"]:
f.seek((p["partNumber"] - 1) * up["partSizeBytes"])
data = f.read(up["partSizeBytes"])
parts.append({"partNumber": p["partNumber"], "etag": put_part(p["url"], data)})
done = requests.post(f"{API}/uploads/{up['id']}/complete", headers=HEADERS, json={"parts": parts})
done.raise_for_status()
return done.json()
except Exception:
requests.delete(f"{API}/uploads/{up['id']}", headers=HEADERS)
raise
use Illuminate\Support\Facades\Http;
function segmntUpload(string $path, string $title): array
{
$api = 'https://test-api.segmnt.cloud/v1/projects/' . config('services.segmnt.project');
$client = fn () => Http::withToken(config('services.segmnt.key'))->acceptJson();
$size = filesize($path);
$up = $client()
->withHeaders(['Idempotency-Key' => (string) \Illuminate\Support\Str::uuid()])
->post("$api/uploads", [
'title' => $title,
'fileName' => basename($path),
'sizeBytes' => $size,
'contentType' => 'video/mp4',
])->throw()->json();
try {
$parts = [];
$file = fopen($path, 'rb');
foreach ($up['parts'] as $p) {
fseek($file, ($p['partNumber'] - 1) * $up['partSizeBytes']);
$data = fread($file, $up['partSizeBytes']);
// No Authorization header on the part URL.
$res = Http::retry(5, 1000)->withBody($data, 'application/octet-stream')
->put($p['url'])->throw();
$parts[] = ['partNumber' => $p['partNumber'], 'etag' => $res->header('ETag')];
}
fclose($file);
return $client()->post("$api/uploads/{$up['id']}/complete", ['parts' => $parts])
->throw()->json();
} catch (\Throwable $e) {
$client()->delete("$api/uploads/{$up['id']}");
throw $e;
}
}

ملاحظة: يقرأ مثال PHP كل جزء في الذاكرة (100 مبيبايت أو أكثر). فاستخدم memory_limit في PHP لا يقلّ عن 512 ميجابايت، أو اجعل الجزء يُقرأ على دفعات (stream).

في المتصفح، استدعِ «الإنشاء» و«الإكمال» من خادمك (فيبقى مفتاح API سرّياً). وأعطِ المتصفح قائمة parts. ثم يرسل المتصفح PUT لكل file.slice(start, end) إلى رابطه، ويقرأ res.headers.get("ETag")، ويعيد قيم ETag إلى خادمك.