WebAssembly ile Gerçek Zamanlı Video ve Görüntü İşleme: Codec ve Filtre Uygulamaları
Tarayıcıda video işleme dediğinizde bir zamanlar insanlar gülerdi. “Abi bu iş sunucuda yapılır” denir, konu kapanırdı. Ama WebAssembly sahneye çıkınca bu tartışma tamamen değişti. Artık tarayıcı içinde FFmpeg çalıştırabiliyorsunuz, gerçek zamanlı filtre uygulayabiliyorsunuz, hatta codec encode/decode işlemlerini client tarafında halledebiliyorsunuz. Bu yazıda bu dünyanın nasıl işlediğini, nerede işe yaradığını ve üretim ortamında nasıl kullanabileceğinizi anlatacağım.
WebAssembly Video İşlemede Neden Fark Yaratıyor
Klasik web mimarisinde video işleme şöyle çalışırdı: Kullanıcı videoyu yükler, sunucu işler, sonuç geri döner. Bu yaklaşımın sorunları açık: bant genişliği maliyeti, gecikme, sunucu yükü ve ölçekleme problemi. Bir canlı yayın platformu düşünün, on binlerce kullanıcı aynı anda video yüklüyor. Sunucu tarafında transcode işlemi yapmak hem pahalı hem de yavaş.
WebAssembly burada devreye giriyor. WASM, C/C++ veya Rust ile yazılmış kodu tarayıcıda yakın-native hızda çalıştırmanıza olanak tanıyor. FFmpeg gibi devasa kütüphaneler bile WASM’a derlenebiliyor. Sonuç olarak codec işlemleri, filtre uygulamaları ve görüntü manipülasyonu artık tamamen istemci tarafında yapılabiliyor.
Performans tarafına bakarsak: WASM, JavaScript’e kıyasla yoğun hesaplama işlemlerinde 10x ila 50x daha hızlı çalışabilir. Video işleme de tam olarak bu kategoriye giriyor: piksel manipülasyonu, FFT hesaplamaları, matrix transformasyonları.
FFmpeg’i WASM’a Derlemek
Gerçek dünya senaryosu: Bir medya yönetim platformu geliştiriyorsunuz. Kullanıcılar video yüklemeden önce tarayıcıda önizleme ve küçük dönüşümler yapabilmeli. Bunu sunucuya yük bindirmeden çözmeniz gerekiyor.
FFmpeg.wasm projesi bu iş için hazır bir çözüm sunuyor. Ama kendi ortamınız için derlemeyi de bilmek önemli.
# Emscripten SDK kurulumu
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# FFmpeg kaynak kodunu çekin
git clone https://github.com/FFmpeg/FFmpeg.git
cd FFmpeg
# WASM için configure
emconfigure ./configure
--target-os=none
--arch=x86_32
--enable-cross-compile
--disable-x86asm
--disable-inline-asm
--disable-stripping
--disable-programs
--disable-doc
--enable-gpl
--enable-libx264
--extra-cflags="-I/usr/local/include"
--extra-cxxflags="-I/usr/local/include"
--extra-ldflags="-L/usr/local/lib"
--pkg-config-flags="--static"
# Derleme (bu biraz zaman alır)
emmake make -j4
Kendi ortamınızda derlemek istemiyorsanız hazır ffmpeg.wasm paketini kullanabilirsiniz. Ama production ortamında codec lisansları ve güvenlik güncellemeleri açısından kendi derleme pipeline’ınızı kurmanızı öneririm.
Temel Video İşleme: FFmpeg.wasm ile Başlangıç
Pratik bir örnekle başlayalım. Kullanıcının yüklediği videoyu tarayıcıda MP4’e dönüştürme senaryosu:
# Node.js ortamında ffmpeg.wasm test
npm install @ffmpeg/ffmpeg @ffmpeg/util
// video-processor.js
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
class VideoProcessor {
constructor() {
this.ffmpeg = new FFmpeg();
this.loaded = false;
}
async initialize() {
// WASM dosyalarını CDN'den yükle veya local serve et
const baseURL = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd';
this.ffmpeg.on('log', ({ message }) => {
console.log('[FFmpeg]', message);
});
this.ffmpeg.on('progress', ({ progress, time }) => {
const percentage = Math.round(progress * 100);
console.log(`İşlem: %${percentage} - ${time}ms`);
});
await this.ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
this.loaded = true;
console.log('FFmpeg WASM hazır');
}
async convertToMP4(inputFile) {
if (!this.loaded) await this.initialize();
// Dosyayı WASM virtual filesystem'e yaz
await this.ffmpeg.writeFile('input.webm', await fetchFile(inputFile));
// Dönüştürme işlemi
await this.ffmpeg.exec([
'-i', 'input.webm',
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '23',
'-c:a', 'aac',
'-b:a', '128k',
'output.mp4'
]);
// Sonucu oku
const data = await this.ffmpeg.readFile('output.mp4');
// Temizlik
await this.ffmpeg.deleteFile('input.webm');
await this.ffmpeg.deleteFile('output.mp4');
return new Blob([data.buffer], { type: 'video/mp4' });
}
}
export default VideoProcessor;
Bu kod parçası production’da kullanılabilir düzeyde. Ancak dikkat edilmesi gereken nokta: FFmpeg.wasm single-threaded çalışır, SharedArrayBuffer kullanımı için COOP/COEP header’larını ayarlamanız gerekiyor.
SharedArrayBuffer ve Multi-threading Konfigürasyonu
Multi-threaded FFmpeg.wasm kullanmak için sunucu tarafında doğru header’ları set etmeniz şart:
# Nginx konfigürasyonu
# /etc/nginx/sites-available/video-app.conf
server {
listen 443 ssl;
server_name video.sirketiniz.com;
# WASM multi-threading için zorunlu header'lar
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# WASM MIME type
types {
application/wasm wasm;
}
# WASM dosyaları için agresif caching
location ~* .wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Cross-Origin-Resource-Policy "cross-origin";
gzip off; # WASM dosyaları zaten sıkıştırılmış
}
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Bu header’ları eklemezseniz SharedArrayBuffer erişimi engellenecek ve multi-threaded işleme çalışmayacak. Özellikle uzun video işlemlerinde bu farkı net göreceksiniz.
Gerçek Zamanlı Görüntü Filtresi: Canvas API + WASM
Şimdi daha ilginç bir senaryoya geçelim: webcam görüntüsüne gerçek zamanlı filtre uygulama. Bir video konferans uygulaması ya da sosyal medya benzeri bir şey geliştirdiğinizi düşünün.
Bu senaryo için saf JavaScript yerine Rust ile WASM modülü yazacağız çünkü piksel manipülasyonunda Rust çok daha performanslı:
// src/lib.rs - Rust ile görüntü filtre kütüphanesi
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn apply_grayscale(pixels: &mut [u8]) {
// Her piksel RGBA formatında 4 byte
for i in (0..pixels.len()).step_by(4) {
let r = pixels[i] as f32;
let g = pixels[i + 1] as f32;
let b = pixels[i + 2] as f32;
// İnsan gözünün renk algısına göre ağırlıklı gri dönüşüm
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
pixels[i] = gray;
pixels[i + 1] = gray;
pixels[i + 2] = gray;
// Alpha kanalına dokunmuyoruz
}
}
#[wasm_bindgen]
pub fn apply_blur(pixels: &mut [u8], width: u32, height: u32, radius: u32) {
let w = width as usize;
let h = height as usize;
let r = radius as usize;
let original = pixels.to_vec();
for y in 0..h {
for x in 0..w {
let mut sum_r = 0u32;
let mut sum_g = 0u32;
let mut sum_b = 0u32;
let mut count = 0u32;
// Çevre piksellerin ortalamasını al
let y_start = if y > r { y - r } else { 0 };
let y_end = (y + r + 1).min(h);
let x_start = if x > r { x - r } else { 0 };
let x_end = (x + r + 1).min(w);
for ny in y_start..y_end {
for nx in x_start..x_end {
let idx = (ny * w + nx) * 4;
sum_r += original[idx] as u32;
sum_g += original[idx + 1] as u32;
sum_b += original[idx + 2] as u32;
count += 1;
}
}
let idx = (y * w + x) * 4;
pixels[idx] = (sum_r / count) as u8;
pixels[idx + 1] = (sum_g / count) as u8;
pixels[idx + 2] = (sum_b / count) as u8;
}
}
}
#[wasm_bindgen]
pub fn apply_sepia(pixels: &mut [u8]) {
for i in (0..pixels.len()).step_by(4) {
let r = pixels[i] as f32;
let g = pixels[i + 1] as f32;
let b = pixels[i + 2] as f32;
pixels[i] = ((r * 0.393 + g * 0.769 + b * 0.189) as u32).min(255) as u8;
pixels[i + 1] = ((r * 0.349 + g * 0.686 + b * 0.168) as u32).min(255) as u8;
pixels[i + 2] = ((r * 0.272 + g * 0.534 + b * 0.131) as u32).min(255) as u8;
}
}
Rust kodunu WASM’a derlemek:
# wasm-pack kurulumu
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Derleme
wasm-pack build --target web --out-dir pkg
# Cargo.toml içeriği
cat > Cargo.toml << 'EOF'
[package]
name = "image-filters"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
EOF
Şimdi bu WASM modülünü webcam stream’ine bağlayalım:
// realtime-filter.js
class RealtimeVideoFilter {
constructor(videoElement, canvasElement) {
this.video = videoElement;
this.canvas = canvasElement;
this.ctx = canvasElement.getContext('2d');
this.wasmModule = null;
this.activeFilter = 'none';
this.animationId = null;
}
async loadWasm() {
// wasm-pack ile oluşturulan modülü import et
const { default: init, apply_grayscale, apply_blur, apply_sepia } =
await import('./pkg/image_filters.js');
await init();
this.filters = { apply_grayscale, apply_blur, apply_sepia };
console.log('Görüntü filtre modülü yüklendi');
}
async startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720, frameRate: 30 },
audio: false
});
this.video.srcObject = stream;
this.canvas.width = 1280;
this.canvas.height = 720;
this.video.onloadedmetadata = () => {
this.video.play();
this.processFrame();
};
}
processFrame() {
// Video frame'i canvas'a çiz
this.ctx.drawImage(this.video, 0, 0);
if (this.activeFilter !== 'none' && this.wasmModule) {
// Piksel verisi al
const imageData = this.ctx.getImageData(0, 0, 1280, 720);
const pixels = imageData.data;
// WASM filtresi uygula
switch (this.activeFilter) {
case 'grayscale':
this.filters.apply_grayscale(pixels);
break;
case 'blur':
this.filters.apply_blur(pixels, 1280, 720, 3);
break;
case 'sepia':
this.filters.apply_sepia(pixels);
break;
}
// İşlenmiş pikselleri geri yaz
this.ctx.putImageData(imageData, 0, 0);
}
// Bir sonraki frame'i işle (60fps hedefi)
this.animationId = requestAnimationFrame(() => this.processFrame());
}
setFilter(filterName) {
this.activeFilter = filterName;
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.video.srcObject) {
this.video.srcObject.getTracks().forEach(track => track.stop());
}
}
}
Codec Uygulaması: H.264 Encode Pipeline
Gerçek bir üretim senaryosunu ele alalım: canlı yayın platformunda tarayıcıdan H.264 encode yapıp RTMP sunucusuna gönderme.
// h264-encoder.js
class BrowserH264Encoder {
constructor() {
this.encoder = null;
this.chunks = [];
}
async initialize(width, height, bitrate) {
// WebCodecs API varsa önce onu dene, yoksa WASM fallback
if ('VideoEncoder' in window) {
await this.initWebCodecs(width, height, bitrate);
} else {
await this.initWasmEncoder(width, height, bitrate);
}
}
async initWebCodecs(width, height, bitrate) {
this.encoder = new VideoEncoder({
output: (chunk, metadata) => {
this.handleEncodedChunk(chunk, metadata);
},
error: (error) => {
console.error('Encoder hatası:', error);
}
});
this.encoder.configure({
codec: 'avc1.42E01E', // H.264 Baseline Profile
width: width,
height: height,
bitrate: bitrate,
framerate: 30,
latencyMode: 'realtime', // Canlı yayın için düşük gecikme modu
bitrateMode: 'constant'
});
console.log('WebCodecs H.264 encoder başlatıldı');
}
handleEncodedChunk(chunk, metadata) {
const buffer = new ArrayBuffer(chunk.byteLength);
chunk.copyTo(buffer);
this.chunks.push({
data: buffer,
timestamp: chunk.timestamp,
isKeyFrame: chunk.type === 'key',
metadata: metadata
});
// WebSocket ile streaming sunucusuna gönder
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(buffer);
}
}
async encodeFrame(videoFrame) {
if (!this.encoder) {
throw new Error('Encoder başlatılmamış');
}
// Her 2 saniyede bir keyframe zorla
const forceKeyframe = (videoFrame.timestamp % 2000000) === 0;
this.encoder.encode(videoFrame, { keyFrame: forceKeyframe });
videoFrame.close(); // Bellek sızıntısını önle
}
async flush() {
await this.encoder.flush();
}
destroy() {
if (this.encoder) {
this.encoder.close();
}
}
}
Performance Profiling ve Optimizasyon
Production ortamında video işleme performansını izlemek kritik. İşte bir monitoring yaklaşımı:
# Chrome DevTools'da WASM profiling için
# Chrome'u aşağıdaki flag ile başlatın
google-chrome --enable-features=WebAssemblyBaseline
--js-flags="--wasm-opt --liftoff"
--enable-webassembly-streaming
# WASM modülünüzün boyutunu kontrol edin
wasm-opt -O3 -o optimized.wasm input.wasm
ls -lh *.wasm
# wasm-opt ile detaylı analiz
wasm-opt --metrics input.wasm
Uygulama tarafında frame drop ve gecikme metriklerini toplamak için:
// performance-monitor.js
class WasmVideoMetrics {
constructor() {
this.frameCount = 0;
this.droppedFrames = 0;
this.processingTimes = [];
this.lastFrameTime = performance.now();
}
recordFrame(processingStartTime) {
const now = performance.now();
const processingTime = now - processingStartTime;
const timeSinceLastFrame = now - this.lastFrameTime;
this.frameCount++;
this.processingTimes.push(processingTime);
// 33ms'den uzun süren frame'ler drop kabul ediliyor (30fps)
if (timeSinceLastFrame > 33) {
this.droppedFrames++;
}
this.lastFrameTime = now;
// Son 100 frame'in ortalamasını hesapla
if (this.processingTimes.length > 100) {
this.processingTimes.shift();
}
}
getStats() {
const avgProcessingTime = this.processingTimes.reduce((a, b) => a + b, 0)
/ this.processingTimes.length;
return {
totalFrames: this.frameCount,
droppedFrames: this.droppedFrames,
dropRate: ((this.droppedFrames / this.frameCount) * 100).toFixed(2) + '%',
avgProcessingTimeMs: avgProcessingTime.toFixed(2),
estimatedFPS: (1000 / avgProcessingTime).toFixed(1)
};
}
}
Edge Computing ile Dağıtık Video İşleme
Cloudflare Workers veya Fastly Compute@Edge gibi edge platformlarında WASM çalıştırabilirsiniz. Bu yaklaşım ile video processing işini kullanıcıya en yakın edge node’da yapabilirsiniz.
# Cloudflare Workers için wrangler kurulumu
npm install -g wrangler
# Yeni Workers projesi
wrangler init video-edge-processor
cd video-edge-processor
# wrangler.toml konfigürasyonu
cat > wrangler.toml << 'EOF'
name = "video-edge-processor"
main = "src/index.js"
compatibility_date = "2024-01-01"
[build]
command = "npm run build"
[[rules]]
type = "CompiledWasm"
globs = ["**/*.wasm"]
EOF
# Workers deployment
wrangler deploy
// src/index.js - Cloudflare Worker
import thumbnailWasm from './thumbnail-generator.wasm';
export default {
async fetch(request, env) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const contentType = request.headers.get('Content-Type');
if (!contentType?.includes('video/')) {
return new Response('Video content bekleniyor', { status: 400 });
}
// WASM modülünü instantiate et
const wasmInstance = await WebAssembly.instantiate(thumbnailWasm);
const { generate_thumbnail, get_result_ptr, get_result_size } =
wasmInstance.exports;
// Video verisini al
const videoData = new Uint8Array(await request.arrayBuffer());
// WASM memory'ye yaz ve thumbnail üret
const memory = wasmInstance.exports.memory;
const inputPtr = wasmInstance.exports.alloc(videoData.length);
new Uint8Array(memory.buffer).set(videoData, inputPtr);
generate_thumbnail(inputPtr, videoData.length, 320, 180);
// Sonucu oku
const resultPtr = get_result_ptr();
const resultSize = get_result_size();
const thumbnailData = new Uint8Array(memory.buffer, resultPtr, resultSize);
return new Response(thumbnailData, {
headers: {
'Content-Type': 'image/jpeg',
'Cache-Control': 'public, max-age=86400',
'CF-Cache-Status': 'DYNAMIC'
}
});
}
};
Üretim Ortamında Dikkat Edilmesi Gerekenler
Buraya kadar anlattıklarımı production’a taşırken mutlaka göz önünde bulundurmanız gereken noktalar var.
Bellek yönetimi en kritik konuların başında geliyor. WASM modülleri linear memory kullanır ve garbage collector yoktur. Video işlemede büyük buffer’lar oluşturuyorsunuz ve bunları manuel serbest bırakmazsanız bellek sızıntısı kaçınılmaz. Uzun süre açık kalan sekmelerde bu durum ciddi problem yaratır.
Fallback stratejisi şart. Her tarayıcı WASM’ı desteklemiyor ve destekleyenler arasında da performans farkları büyük. Özellikle mobil cihazlarda ısınma ve throttling sorunlarıyla karşılaşabilirsiniz. Her zaman sunucu taraflı bir fallback senaryonuz olsun.
WASM dosya boyutu önemli. FFmpeg.wasm full build 30MB’ın üzerinde. Kullanıcılar bu dosyayı her oturumda indirmek zorunda kalmasın; agresif cache stratejisi ve Service Worker ile bu sorunu çözebilirsiniz.
Güvenlik tarafında ise WASM modüllerini integrity check ile sunun. Bir CDN’den çekilen WASM dosyasının değiştirilip değiştirilmediğini subresource integrity ile doğrulayabilirsiniz.
Cross-origin izolasyon gereksinimleri bazı üçüncü parti script’lerle çakışabilir. COOP/COEP header’larını ekleyince Google Analytics, reklam scriptleri ve bazı sosyal medya widget’ları kırılabilir. Bunu erkenden test edin.
Sonuç
WebAssembly, video ve görüntü işlemeyi tarayıcıda gerçekten pratik bir seçenek haline getirdi. FFmpeg’i tarayıcıda çalıştırmak, gerçek zamanlı filtre uygulamak, H.264 encode yapmak; bunların hepsi artık production’da kullanılabilir teknolojiler. Edge computing ile birleşince kullanıcıya en yakın noktada işlem yapabiliyorsunuz, bu da hem gecikmeyi azaltıyor hem de merkezi sunucu maliyetlerini düşürüyor.
Ama şunu da net söyleyeyim: her şeyi tarayıcıya taşımak doğru değil. 4K video transcode, makine öğrenmesi tabanlı video analizi gibi ağır işler için hala güçlü sunuculara ihtiyacınız var. WASM’ın gücünü sunucu tarafını tamamen ortadan kaldırmak için değil, sunucu yükünü azaltmak ve kullanıcı deneyimini iyileştirmek için kullanın.
Başlangıç için ffmpeg.wasm ile küçük bir proof-of-concept yapın, performance metriklerini toplayın, ardından gerçek ihtiyacınıza göre derinleştirin. Rust ile kendi filtre kütüphanenizi yazmak kulağa karmaşık geliyor ama wasm-pack ile bu süreç düşündüğünüzden çok daha pürüzsüz.
