الرفع المباشر
يرسل تطبيقك ملف الفيديو مباشرة إلى التخزين. وخادمك لا ينقل الملف. والرفع 4 خطوات:
- الإنشاء (Create). تحصل على رابط موقّع واحد لكل جزء.
- الرفع (PUT). ارفع كل جزء إلى رابطه. واحفظ ترويسة
ETagمن كل رد. - الإكمال (Complete). أكمل الرفع بقائمة أرقام الأجزاء وقيم
ETag. - نعالج الفيديو. استعلم عن حالة الفيديو حتى تصبح
ready(يأتي إشعارvideo.readyمع إصدار المعالجة).
الروابط الموقّعة للرفع فقط. أمّا التشغيل فيستخدم رموز تشغيل موقّعة منفصلة. ويأتي دليلها عند إطلاق التشغيل الموقّع.
قبل أن تبدأ
Section titled “قبل أن تبدأ”- معرّف مشروع (
prj_...) ومفتاح API لذلك المشروع (sk_...). - عنوان الأساس في TEST:
https://test-api.segmnt.cloud - أرسل المفتاح في ترويسة
Authorization: Bearer <key>في نداءات API. ولا ترسله إلى روابط الأجزاء.
الحدود
Section titled “الحدود”| البند | القيمة |
|---|---|
| أنواع الملفات | 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.
الخطوة 1: أنشئ الرفع
Section titled “الخطوة 1: أنشئ الرفع”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 في الوقت نفسه بدايةً جيدة) وبأي ترتيب.
الخطوة 3: أكمل
Section titled “الخطوة 3: أكمل”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 مع الفيديو
الحالي.
الخطوة 4: انتظر النتيجة
Section titled “الخطوة 4: انتظر النتيجة”تنتقل حالة الفيديو إلى processing ثم ready (أو failed). استعلم عبر
GET /v1/projects/{projectId}/videos/{videoId} حتى تتغيّر الحالة. وإشعار
video.ready ليس مباشراً بعد؛ فهو يُطلق مع إصدار المعالجة، وسيظهره هذا الدليل حينها.
وفي حالات نادرة قد يحتفظ فريقنا بالفيديو للمراجعة (الحالة held). ولا يُشغَّل حتى
ينتهي الاحتفاظ.
إلغاء رفع
Section titled “إلغاء رفع”curl -X DELETE "$BASE/v1/projects/$PROJECT/uploads/$UPLOAD" \ -H "Authorization: Bearer $SEGMNT_KEY"الرد: 204. وتُحذف الأجزاء والفيديو. وإعادة النداء آمنة.
وإذا كان الرفع مكتملاً بالفعل، تحصل على 409 upload_closed.
والرفوعات غير المكتملة تُحذف بعد expiresAt.
الأخطاء
Section titled “الأخطاء”الأخطاء تستخدم 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 للأجزاء تأتي من التخزين، لا من واجهتنا. انظر القسم التالي.
قواعد إعادة المحاولة
Section titled “قواعد إعادة المحاولة”| النداء | آمن للتكرار؟ | الطريقة |
|---|---|---|
| Create | نعم، مع Idempotency-Key |
المفتاح نفسه يعيد الرفع نفسه. وبدون مفتاح، ينشئ التكرار فيديو ثانياً. |
PUT للجزء |
نعم | أرسل الجزء نفسه مرّة أخرى. والأخير يفوز. استخدم أحدث ETag. وأعِد المحاولة عند أخطاء الشبكة و5xx و429 بانتظار متزايد (1 ثانية، 2 ثانية، 4 ثوانٍ). |
PUT للجزء يعيد 403 |
لا | انتهت صلاحية الرابط. ابدأ رفعاً جديداً. |
| Complete | نعم | النص نفسه. وإذا ضاع الرد، أرسله مرّة أخرى. |
| Cancel | نعم | دائماً 204 لرفع مُلغى. |
لا تُعِد المحاولة عند 400 و401 و403 و404 من واجهتنا دون تغيير.
أمثلة برمجية
Section titled “أمثلة برمجية”جُرّبت هذه الأمثلة على بيئة TEST. فكل من أمثلة Node وPython وPHP رفع ملفاً بحجم 250 مبيبايت (3 أجزاء) ووصل الفيديو إلى حالة
processing. كما تغطّي اختباراتنا مسارات الإنشاء والإكمال والإلغاء والأخطاء.
Node.js (18+)
Section titled “Node.js (18+)”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(); }}Python 3 (pip install requests)
Section titled “Python 3 (pip install requests)”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) raisePHP / Laravel
Section titled “PHP / Laravel”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).
المتصفح
Section titled “المتصفح”في المتصفح، استدعِ «الإنشاء» و«الإكمال» من خادمك (فيبقى مفتاح API سرّياً).
وأعطِ المتصفح قائمة parts. ثم يرسل المتصفح PUT لكل
file.slice(start, end) إلى رابطه، ويقرأ res.headers.get("ETag")، ويعيد قيم
ETag إلى خادمك.