פיתוח מערכת העלאת וידאו והמרת פורמטים (Transcoding) במפתח מלא

העלאת וידאו לאתר הופכת פעמים רבות לצוואר בקבוק: השרת לא עומד בעומס, והמשתמשים מקבלים שגיאות במקום קובץ מוכן. אנחנו בונים מערכת העלאה והמרת וידאו (transcoding) במפתח מלא, שמעבירה את העיבוד לפייפליין אסינכרוני ואינה חוסמת את האתר. הצוות שלנו מוביל את הפרויקט מהארכיטקטורה ועד היישום, ומבטיח פעילות אמינה עם תמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
פיתוח מערכת העלאת וידאו והמרת פורמטים (Transcoding) במפתח מלא
מורכב
מ- 1 שבוע עד 3 חודשים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1504
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

לקוחות מעלים וידאו בפורמטים שונים — מ-MP4 מטלפונים ועד MOV ממצלמות מקצועיות. אם ממירים תוך כדי תנועה, השרת קורס תחת העומס, בקשות נופלות בזמן אאוט, ומשתמשים מקבלים קבצים פגומים. ללא ארכיטקטורה נכונה, העלאת וידאו הופכת לצוואר בקבוק עבור כל אתר מדיה. אנו מפתחים צינור אסינכרוני: העלאה חתומה מראש לענן עוקפת את השרת מיד, וההמרה מתבצעת בתור על עובדים ייעודיים או שירותים מנוהלים.

תרחיש טיפוסי: משתמש מצלם 4K ב-60fps (500 MB). העלאה ישירה לשרת גורמת ל-nginx timeout. כתובת חתומה מראש פותרת זאת — הקובץ עובר ל-S3 תוך כמה דקות, והשרת רק מאשר. לאחר מכן, עובד מרים את הקובץ וממיר ברקע, מבלי לחסום את המשתמש. ההתקדמות מוצגת דרך WebSocket — המשתמש רואה סטטוס בזמן אמת.

למה להעביר את ההמרה לתור רקע?

המרה היא פעולה אינטנסיבית למעבד. אפילו וידאו קצר ב-1080p מריץ את המעבד ב-100% למשך דקות. אם זה נעשה בתהליך web, בקשות אחרות נחסמות. הצינור שלנו משתמש ב-S3 Event → SQS/Lambda → עובדים עם FFmpeg. זה מאפשר קנה מידה אופקי מבלי לפגוע בזמן התגובה של האתר. עומס המעבד יורד ב-90% כשמעבירים לתור.

אילו בעיות מערכת העלאת והמרת הווידאו פותרת?

  • חסימת מעבד: העיבוד לא משפיע על שרת ה-web.
  • רזולוציות מרובות: יצירה אוטומטית של 360p, 720p, 1080p ו-WebM לכיסוי כל המכשירים.
  • העלאה איטית של משתמשים: קישור חתום מראש מאפשר להם להעלות ישירות לענן, תוך עקיפת השרת; מעקב התקדמות דרך WebSocket מעדכן אותם.

איך אנו בונים את מערכת העלאת והמרת הווידאו

אנו משתמשים בכתובות חתומות מראש מ-AWS S3: הלקוח מקבל קישור זמני ושולח את הקובץ ישירות ל-bucket. לאחר סיום ההעלאה, השרת כותב רשומת DB ומכניס משימה לתור (למשל, דרך SQS או Laravel Horizon).

ארכיטקטורת הצינור

[Client] -> [Presigned S3 Upload] -> [S3: original/]
| [S3 Event -> SQS/Lambda] -> [Transcoding Worker (FFmpeg)]
| [S3: processed/{quality}/] -> [CDN CloudFront]
| [Update DB: video.status = ready, paths = {...}]
| [WebSocket/Webhook -> Client notification]

שלב 1: העלאה חתומה מראש ל-S3

// Генерация presigned URL для прямой загрузки с клиента
class VideoController extends Controller
{
    public function initiateUpload(Request $request): JsonResponse
    {
        $request->validate([
            'filename' => 'required|string|max:255',
            'content_type' => 'required|in:video/mp4,video/webm,video/quicktime,video/x-msvideo',
            'size' => 'required|integer|max:5368709120', // 5 GB
        ]);

        $key = sprintf(
            'original/%d/%s/%s',
            auth()->id(),
            now()->format('Y/m'),
            Str::uuid() . '.' . pathinfo($request->filename, PATHINFO_EXTENSION)
        );

        $s3 = app('aws')->createClient('s3');
        $command = $s3->getCommand('PutObject', [
            'Bucket' => config('filesystems.disks.s3.bucket'),
            'Key' => $key,
            'ContentType' => $request->content_type,
        ]);
        $presigned = $s3->createPresignedRequest($command, '+2 hours');

        // Создаём запись в БД со статусом pending
        $video = Video::create([
            'user_id' => auth()->id(),
            'original_key' => $key,
            'original_name' => $request->filename,
            'status' => 'pending',
            'size' => $request->size,
        ]);

        return response()->json([
            'video_id' => $video->id,
            'upload_url' => (string) $presigned->getUri(),
            'key' => $key,
        ]);
    }

    // Клиент вызывает после успешной загрузки
    public function confirmUpload(Request $request, Video $video): JsonResponse
    {
        $this->authorize('update', $video);
        $video->update(['status' => 'uploaded']);
        TranscodeVideoJob::dispatch($video);
        return response()->json(['status' => 'processing']);
    }
}

שלב 2: המרה עם FFmpeg

class TranscodeVideoJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public int $timeout = 7200; // 2 часа
    public int $tries = 2;

    const QUALITIES = [
        '360p' => ['width' => 640, 'height' => 360, 'bitrate' => '800k', 'audiorate' => '96k'],
        '720p' => ['width' => 1280, 'height' => 720, 'bitrate' => '2500k', 'audiorate' => '128k'],
        '1080p' => ['width' => 1920, 'height' => 1080, 'bitrate' => '5000k', 'audiorate' => '192k'],
    ];

    public function __construct(private Video $video) {}

    public function handle(): void
    {
        $this->video->update(['status' => 'transcoding']);

        // Скачать оригинал во временный файл
        $tempInput = tempnam(sys_get_temp_dir(), 'video_') . '.mp4';
        Storage::disk('s3')->copy($this->video->original_key, $tempInput); // упрощённо

        // Реально: stream из S3 через signed URL
        $inputUrl = Storage::disk('s3')->temporaryUrl($this->video->original_key, now()->addHour());

        $paths = [];

        foreach (self::QUALITIES as $quality => $params) {
            $outputKey = sprintf(
                'processed/%d/%s/%s.mp4',
                $this->video->user_id,
                $this->video->id,
                $quality
            );

            $outputPath = sys_get_temp_dir() . "/{$this->video->id}_{$quality}.mp4";

            $scale = "scale={$params['width']}:{$params['height']}:force_original_aspect_ratio=decrease,pad={$params['width']}:{$params['height']}:(ow-iw)/2:(oh-ih)/2";

            $command = [
                'ffmpeg',
                '-y',
                '-i', $inputUrl,
                '-vf', $scale,
                '-c:v', 'libx264',
                '-preset', 'medium', // баланс скорость/качество
                '-crf', '23',
                '-maxrate', $params['bitrate'],
                '-bufsize', (int)($params['bitrate']) * 2 . 'k',
                '-c:a', 'aac',
                '-b:a', $params['audiorate'],
                '-movflags', '+faststart', // для web streaming
                $outputPath,
            ];

            $process = new \Symfony\Component\Process\Process($command);
            $process->setTimeout(3600);
            $process->run();

            if (!$process->isSuccessful()) {
                throw new \RuntimeException("FFmpeg failed for {$quality}: " . $process->getErrorOutput());
            }

            // Загрузить в S3
            Storage::disk('s3')->putFileAs(
                dirname($outputKey),
                new \Illuminate\Http\File($outputPath),
                basename($outputKey),
            );

            $paths[$quality] = $outputKey;
            unlink($outputPath);
        }

        // Генерация thumbnail из 10% длительности
        $thumbKey = "thumbnails/{$this->video->user_id}/{$this->video->id}.jpg";
        $thumbPath = sys_get_temp_dir() . "/{$this->video->id}_thumb.jpg";

        $process = new \Symfony\Component\Process\Process([
            'ffmpeg',
            '-y',
            '-i', $inputUrl,
            '-ss', '10%', // 10% от длительности
            '-vframes', '1',
            '-q:v', '2',
            $thumbPath,
        ]);
        $process->run();

        if (file_exists($thumbPath)) {
            Storage::disk('s3')->putFileAs(
                dirname($thumbKey),
                new \Illuminate\Http\File($thumbPath),
                basename($thumbKey),
            );
        }

        // Получить метаданные
        $ffprobe = \FFMpeg\FFProbe::create();
        $duration = $ffprobe->streams($inputUrl)->videos()->first()->get('duration');
        $resolution = $ffprobe->streams($inputUrl)->videos()->first()->getDimensions();

        $this->video->update([
            'status' => 'ready',
            'paths' => $paths,
            'thumbnail_key' => $thumbKey,
            'duration' => (int) $duration,
            'width' => $resolution->getWidth(),
            'height' => $resolution->getHeight(),
        ]);

        // Уведомить пользователя
        $this->video->user->notify(new VideoReadyNotification($this->video));
    }

    public function failed(\Throwable $e): void
    {
        $this->video->update(['status' => 'failed', 'error' => $e->getMessage()]);
        Log::error('Video transcoding failed', ['video_id' => $this->video->id, 'error' => $e->getMessage()]);
    }
}

באיזו גישה לבחור: FFmpeg על תשתית עצמית או AWS MediaConvert?

FFmpeg נותן שליטה מלאה וללא עלויות AWS נוספות, אבל צריך לנהל שרתים ותורים. MediaConvert הוא שירות מנוהל: משלמים לפי דקת המרה, ללא דאגות קנה מידה. לסטארטאפים ופרויקטים בינוניים, FFmpeg בתור (למשל, Laravel Horizon) הוא ידידותי לתקציב. לקטלוגי וידאו גדולים עם עומס גבוה, MediaConvert משתלם יותר: הוא מתמודד עם קפיצות עומס אוטומטית ותומך ב-HLS. לנפחים עד 1000 דקות בחודש, FFmpeg על תשתית עצמית זול פי שלושה מ-MediaConvert, וחוסך עד $30 בחודש.

קריטריון FFmpeg + תור AWS MediaConvert
שליטה שליטה מלאה בפרמטרים ובקודקים מוגבל לפרסטים
קנה מידה דורש הגדרת autoscaling לעובדים קנה מידה אוטומטי
עלות ל-1000 דקות/חודש $0 (רק שרת) ~$30 (לפי תעריף)
זמן הגדרה 2–3 ימים יום אחד

MediaConvert מהיר פי שניים בנפחים גדולים בזכות עיבוד מקבילי, אבל FFmpeg זול פי שלושה לנפחים קטנים.

AWS Elastic Transcoder / MediaConvert

import boto3

def transcode_with_mediaconvert(input_key: str, output_prefix: str) -> str:
    client = boto3.client('mediaconvert', region_name='eu-west-1', endpoint_url='https://abc123.mediaconvert.eu-west-1.amazonaws.com')
    job = client.create_job(
        Role='arn:aws:iam::123456789:role/MediaConvertRole',
        Settings={
            'Inputs': [
                {
                    'FileInput': f's3://my-bucket/{input_key}',
                    'AudioSelectors': {'Audio Selector 1': {'DefaultSelection': 'DEFAULT'}},
                    'VideoSelector': {},
                }
            ],
            'OutputGroups': [
                {
                    'Name': 'File Group',
                    'OutputGroupSettings': {
                        'Type': 'FILE_GROUP_SETTINGS',
                        'FileGroupSettings': {
                            'Destination': f's3://my-bucket/{output_prefix}/',
                        },
                    },
                    'Outputs': [
                        {
                            'NameModifier': '_720p',
                            'VideoDescription': {
                                'Width': 1280,
                                'Height': 720,
                                'CodecSettings': {
                                    'Codec': 'H_264',
                                    'H264Settings': {'Bitrate': 2500000, 'RateControlMode': 'CBR'},
                                },
                            },
                            'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 128000}}}],
                            'ContainerSettings': {'Container': 'MP4'},
                        },
                        # ... 360p, 1080p аналогично
                    ],
                }
            ],
        }
    )
    return job['Job']['Id']

התקדמות ההמרה

// Отдавать прогресс через SSE или WebSocket
Route::get('/videos/{video}/status', function (Video $video) {
    return response()->json([
        'status' => $video->status,
        'progress' => $video->transcoding_progress,
        'paths' => $video->status === 'ready' ? $video->paths : null,
    ]);
});

תהליך העבודה

  1. ניתוח: איסוף סוגי וידאו, נפח צפוי, דרישות לנגינה אדפטיבית.
  2. עיצוב: בחירת S3 מול אחסון עצמי, הגדרת פרמטרי FFmpeg (bitrate, CRF, פרסטים).
  3. יישום: יצירת כתובות חתומות מראש, כתיבת עובדי תור, הגדרת WebSocket.
  4. בדיקות: בדיקת עומס עם גדלי וידאו שונים, אימות איכות תמונות ממוזערות ואודיו.
  5. פריסה: הגדרת ניטור (התראות על שגיאות המרה), CI/CD לעדכוני צינור.

לוח זמנים

משימה זמן
העלאה חתומה מראש + FFmpeg בתור 4–5 ימים
יצירת תמונות ממוזערות + מטא-דאטה +1–2 ימים
אינטגרציית AWS MediaConvert 2–3 ימים
HLS סטרימינג אדפטיבי +3–4 ימים (משימה נפרדת)

מה כלול

  • תיעוד ארכיטקטורת הצינור.
  • קוד מקור עם הערות (Laravel + FFmpeg או Python + MediaConvert).
  • הגדרת גישת S3, קונפיגורציית תפקידי IAM.
  • סקריפטים לניטור והתראות.
  • הכשרת צוות לתמיכה.
  • זמן תגובה מובטח לכרטוסי תמיכה (SLA).
  • אינטגרציה מוסמכת עם ספקי ענן מרכזיים (AWS, GCP, Azure).
  • 5+ שנות ניסיון בבניית צינורות וידאו, עם למעלה מ-30 פרויקטים מוצלחים לאתרי מדיה ופלטפורמות חינוכיות.

טעויות נפוצות בפועל: לא להגדיר timeout למשימות — המרה יכולה לקחת שעות ועובדים קורסים; לא לטפל בשגיאות העלאה (הפסקת אינטרנט, content-type שגוי); כתובת חתומה מראש קצרה מדי — המשתמש לא מספיק לסיים את ההעלאה.

אנו מעריכים את הפרויקט שלך ביום אחד — פשוט צור קשר. קבל ייעוץ על ארכיטקטורת צינור וידאו. השאר בקשה כדי לקבל הערכה תוך יום אחד.