বাফার ও ABR বাস্তবায়ন
বাফার টার্গেট, ব্যাক বাফার, থ্রুপুট মাপার আসল কৌশল, BOLA আর হাইব্রিড, ভিউপোর্ট ক্যাপিং, বাফারের ফাঁক পার হওয়া, TV-র মেমরি সীমা, আর খারাপ নেটওয়ার্ক বানিয়ে প্লেয়ার পরীক্ষা করা।
গল্পে বুঝি
মিরপুরের একটা পাঁচতলা বাড়ির ছাদে একটা পানির ট্যাংক। নিচে রাস্তার লাইন থেকে একটা পাম্প পানি তোলে, আর ট্যাংক থেকে পাইপ দিয়ে পানি নামে প্রতিটা ফ্ল্যাটের কলে। বাড়ির কেয়ারটেকার হাইসাম। তার একটাই কাজ: কারো কল যেন কখনো শুকিয়ে না যায়। সমস্যা হলো রাস্তার লাইনের চাপ সারাদিন এক থাকে না। সকালে ভালো, দুপুরে কম, সন্ধ্যায় প্রায় নেই। আর বাড়ির লোকেরা কী ব্যবহার করবে সেটাও হাইসামের হাতে কিছুটা আছে: সে প্রতিটা ফ্ল্যাটের কলের মুখে বড় বা ছোট শাওয়ারহেড বসাতে পারে। বড় শাওয়ারে গোসল আরামের, কিন্তু পানি যায় তাড়াতাড়ি।
প্রথম কয়েক মাস হাইসাম ভুল করত। পাম্প চালিয়ে এক বালতি ভরতে কত সময় লাগল সেটা দেখে সে লাইনের চাপ আন্দাজ করত। কিন্তু বালতি ছোট হলে মাপটা সবসময় কম আসত, কারণ পাম্প চালু হতেই কয়েক সেকেন্ড লাগে পুরো জোর পেতে। আবার একদিন পাশের বাড়ির জমা পানি থেকে এক বালতি ধার নিয়ে সে ভাবল লাইনের চাপ দারুণ, সব ফ্ল্যাটে বড় শাওয়ার লাগিয়ে দিল, আর সন্ধ্যায় ট্যাংক শুকিয়ে গেল। তখন সে নিয়ম বদলাল। এক বালতির মাপ বিশ্বাস না করে গত কয়েকবারের মাপের একটা চলমান গড় রাখে, দুটো: একটা দ্রুত বদলায়, একটা ধীরে, আর দুটোর মধ্যে যেটা কম সেটা ধরে। বড় শাওয়ার লাগানোর আগে সে অনেক বেশি নিশ্চিত হতে চায়, কিন্তু ছোট শাওয়ারে নামাতে দেরি করে না। আর বড় শাওয়ার লাগানো আর খোলার মাঝে অন্তত কয়েক দিন অপেক্ষা করে, নইলে লোকে বিরক্ত হয়।
ট্যাংকে একটা ফ্লোট সুইচ আছে: পানি নির্দিষ্ট উচ্চতায় উঠলে পাম্প বন্ধ। ট্যাংক যত বড়ই হোক, পুরোটা ভরার মানে নেই, কারণ পানি জমে থাকলে নষ্ট হয় আর বিদ্যুৎ বিল ওঠে। পাশের টিনশেড বাড়ির ট্যাংক অনেক ছোট, সেখানে ফ্লোট সুইচ আরো নিচে রাখতে হয়, নইলে উপচে পড়ে। একবার ট্যাংকের পাইপে একটা বাতাসের বুদবুদ আটকে গেল: ট্যাংকে পানি আছে, কিন্তু কলে আসছে না। হাইসাম পাইপে হালকা টোকা দিল, বুদবুদ সরে গেল, পানি চলল। তিনবার টোকা দিয়েও না সরলে সে মিস্ত্রি ডাকে।
মিলিয়ে নিই: ট্যাংকের পানি হলো বাফার, কলের পানি খরচ হলো প্লেব্যাক, আর পাম্পের তোলার হার হলো নেটওয়ার্ক থ্রুপুট। বড় বা ছোট শাওয়ারহেড হলো রেন্ডিশন বাছাই, অর্থাৎ ABR। ফ্লোট সুইচ হলো বাফার গোল (maxBufferLength, bufferingGoal), ছোট ট্যাংক হলো TV-র মেমরি সীমা। ছোট বালতিতে কম মাপ আসা হলো সেগমেন্ট সাইজের বায়াস, ধার করা পানির বালতি হলো ক্যাশ থেকে আসা সেগমেন্টের ভুয়া দ্রুত মাপ, আর দ্রুত ও ধীর দুই গড়ের কমটা নেওয়া হলো দুই EWMA-র মিনিমাম। বড় শাওয়ারে ওঠার আগে বেশি নিশ্চিত হওয়া হলো আপ-সুইচের কড়া ফ্যাক্টর, আর কয়েক দিন অপেক্ষা হলো সুইচ ইন্টারভাল। পাইপের বুদবুদ হলো বাফারের ফাঁক (buffer hole), টোকা দেওয়া হলো প্লেহেড নাজ (nudge), আর তিনবারের পর মিস্ত্রি ডাকা হলো ফেটাল স্টল এরর। বাস্তবে hls.js, Shaka Player আর dash.js ঠিক এই কাজগুলোই করে, শুধু প্রতিটার নাম আর ডিফল্ট সংখ্যা আলাদা।
এই চ্যাপ্টার কোথা থেকে শুরু করে
বাফার মডেল, থ্রুপুট বনাম বাফার-ভিত্তিক ABR, আর হাইব্রিড কেন জেতে, সেই ধারণাগুলো আছে ভিডিও ট্র্যাকের প্লেব্যাক ও ABR চ্যাপ্টারে, সাথে একটা পুরো ABR কন্ট্রোলারের কোড। এখানে আমরা সেটা আবার শেখাব না। এখানে প্রশ্নটা ব্যবহারিক: আসল প্লেয়ার লাইব্রেরিগুলো এই ধারণাগুলো কোন সংখ্যা দিয়ে, কোন কনফিগ দিয়ে বাস্তবায়ন করে, আর আপনার কনটেন্ট ও দর্শকের জন্য সেগুলো কীভাবে টিউন করবেন।
এই চ্যাপ্টারের সব ডিফল্ট মান লাইব্রেরির ডকস থেকে নেওয়া, ২০২৬ সালের অক্টোবর অনুযায়ী: hls.js-এর docs/API.md ও src/config.ts, Shaka Player-এর কনফিগ টিউটোরিয়াল, আর dash.js-এর ABR সেটিংস পেজ। ভার্সন বদলালে ডিফল্ট বদলায়। নিজের ভার্সনে hls.config বা player.getConfiguration() কনসোলে প্রিন্ট করে মিলিয়ে নিন।
বাফারের তিনটা সীমা
প্রতিটা প্লেয়ার তিনটা প্রশ্নের উত্তর রাখে:
- সামনে কতটা জমাব? (বাফার গোল)
- কতটা জমলে চালানো শুরু করব? (রিবাফারিং গোল)
- পেছনে, যা দেখা হয়ে গেছে, কতটা রেখে দেব? (ব্যাক বাফার)
তিনটা লাইব্রেরিতে এদের নাম:
| প্রশ্ন | hls.js | Shaka Player | dash.js |
|---|---|---|---|
| সামনে কতটা | maxBufferLength (৩০s), maxBufferSize (৬০ MB) | streaming.bufferingGoal | streaming.buffer.bufferTimeDefault |
| সর্বোচ্চ সীমা | maxMaxBufferLength (৬০০s) | — | — |
| চালু করার আগে কতটা | নিজস্ব হিসাবে, আলাদা কনফিগ নেই | streaming.rebufferingGoal | — |
| পেছনে কতটা | backBufferLength (ডিফল্ট Infinity) | streaming.bufferBehind (৩০s) | streaming.buffer.bufferToKeep |
hls.js-এর মডেলটা একটু অদ্ভুত, তাই আলাদা করে বলা দরকার। সে সেকেন্ড নয়, বাইট ধরে বাফার করতে চায়: ডিফল্টে সামনে ৬০ MB পর্যন্ত। কিন্তু maxBufferLength (৩০ সেকেন্ড) হলো একটা নিশ্চিত ন্যূনতম: ৬০ MB পার হয়ে গেলেও অন্তত ৩০ সেকেন্ড সে জমাবে। আর maxMaxBufferLength (৬০০ সেকেন্ড) হলো ছাদ, কারণ খুব নিচু বিটরেটে ৬০ MB ভরতে এক ঘণ্টার বেশি ভিডিও লাগতে পারে।
4K rendition at 16 Mbps -> 60 MB is about 30 s -> buffers ~30 s
480p at 1 Mbps -> 60 MB is about 480 s -> buffers ~480 s
audio-only at 128 kbps -> 60 MB is about 3,750 s -> capped at 600 s ব্যাক বাফার কেন মাথাব্যথা
backBufferLength-এর ডিফল্ট Infinity মানে hls.js নিজে থেকে দেখা হয়ে যাওয়া অংশ মোছে না; ব্রাউজার নিজের সীমায় পৌঁছালে তখন সে মোছে। ডেস্কটপ Chrome-এ এটা প্রায় কখনো সমস্যা নয়। কিন্তু দুই ঘণ্টার সিনেমা একটা স্মার্ট TV-তে চালালে মেমরি ভরে যায়, আর তখন appendBuffer একটা QuotaExceededError ছোড়ে। পেছনে টানার (backward seek) সুবিধা রাখতে চাইলে ব্যাক বাফার কিছুটা রাখুন, কিন্তু একটা সীমা দিন:
const hls = new Hls({
backBufferLength: 90, // keep 90 s behind the playhead, evict the rest
}); কতটা সামনে জমাবেন, সেটা ব্যবসার সিদ্ধান্তও। দর্শকের বড় অংশ যদি প্রথম দুই মিনিটে চলে যায় (শর্ট ভিডিও, নিউজ ক্লিপ), তাহলে ৩০ সেকেন্ড আগে নামানো মানে প্রতিটা ভিউতে কিছু অপচয় করা ইগ্রেস, যার বিল আপনি দেন (দেখুন CDN-এর খরচের মডেল)। লম্বা সিনেমায় ঠিক উল্টো: বেশি বাফার মানে খারাপ নেটওয়ার্কে কম রিবাফার। এক কনফিগ সব কনটেন্টে বসাবেন না।
থ্রুপুট মাপা: যেখানে সবাই ভুল করে
ABR-এর সিদ্ধান্ত যত বুদ্ধিমানই হোক, তার ইনপুট যদি ভুল হয়, ফল ভুল। থ্রুপুট মাপার তিনটা আসল ফাঁদ:
১. ছোট সেগমেন্টের বায়াস। ২ সেকেন্ডের ৪০০ kbps সেগমেন্ট মানে মোটামুটি ১০০ KB। এত ছোট ফাইলে TCP-র কনজেশন উইন্ডো পুরো খোলার আগেই ডাউনলোড শেষ, আর রিকোয়েস্টের TTFB (প্রথম বাইট আসতে সময়) মোট সময়ের বড় অংশ খেয়ে ফেলে। ফল: মাপা থ্রুপুট আসলের চেয়ে কম, প্লেয়ার নিচেই আটকে থাকে। প্রতিকার: থ্রুপুট হিসাবে TTFB বাদ দিয়ে শুধু বডি ট্রান্সফারের সময় ধরা, আর খুব ছোট নমুনার ওজন কম দেওয়া।
২. ক্যাশের ভুয়া দ্রুত মাপ। একটা সেগমেন্ট যদি ব্রাউজারের HTTP ক্যাশ বা সার্ভিস ওয়ার্কার থেকে আসে, সেটা নেটওয়ার্ক ছাড়াই কয়েক মিলিসেকেন্ডে “নামে”। এমন নমুনা থ্রুপুটে গুনলে প্লেয়ার ভাবে আপনার কাছে গিগাবিট আছে। প্রতিকার: যে নমুনার সময় অবিশ্বাস্য রকম কম, সেটা বাদ দিন।
৩. বাতিল করা ডাউনলোড। সিক বা কোয়ালিটি বদলের সময় যে ডাউনলোড মাঝপথে বাতিল হয়, সেটা থেকে থ্রুপুট হিসাব অর্থহীন। প্রতিকার: শুধু সম্পূর্ণ ডাউনলোড গুনুন।
সেকেন্ডে মাপা হাফ-লাইফের EWMA
hls.js-এর ডিজাইন ডকুমেন্ট অনুযায়ী সে দুটো EWMA রাখে, একটা ছোট হাফ-লাইফের (দ্রুত), একটা লম্বা হাফ-লাইফের (ধীর), আর দুটোর মিনিমামকে অনুমান ধরে। ডিফল্ট হাফ-লাইফ VOD-এ ৩ আর ৯ সেকেন্ড (abrEwmaFastVoD, abrEwmaSlowVoD), লাইভেও তাই (abrEwmaFastLive, abrEwmaSlowLive)। হাফ-লাইফ “সেকেন্ডে” কারণ প্রতিটা নমুনার ওজন হলো সেগমেন্টের দৈর্ঘ্য: ৬ সেকেন্ডের সেগমেন্ট ২ সেকেন্ডের সেগমেন্টের তিনগুণ ওজন পায়। এতে সেগমেন্ট দৈর্ঘ্য বদলালেও আচরণ একই থাকে।
একটা ছোট, সঠিক বাস্তবায়ন:
// Half-life EWMA weighted by media seconds, with zero-start bias correction.
class Ewma {
private alpha: number;
private estimate = 0;
private totalWeight = 0;
constructor(halfLifeSeconds: number) {
this.alpha = Math.exp(Math.log(0.5) / halfLifeSeconds);
}
sample(weightSeconds: number, value: number): void {
const a = Math.pow(this.alpha, weightSeconds);
this.estimate = value * (1 - a) + a * this.estimate;
this.totalWeight += weightSeconds;
}
get value(): number {
// Without this, early estimates are dragged toward the initial 0.
const zeroFactor = 1 - Math.pow(this.alpha, this.totalWeight);
return zeroFactor > 0 ? this.estimate / zeroFactor : 0;
}
}
class BandwidthEstimator {
private fast = new Ewma(3);
private slow = new Ewma(9);
private samples = 0;
constructor(private defaultEstimate = 500_000) {}
// bytes: body size; transferMs: time after first byte; durationS: media seconds
add(bytes: number, transferMs: number, durationS: number): void {
if (transferMs < 10 || bytes < 16_000) return; // cache hits and tiny samples lie
const bps = (bytes * 8 * 1000) / transferMs;
this.fast.sample(durationS, bps);
this.slow.sample(durationS, bps);
this.samples++;
}
get bps(): number {
if (this.samples < 2) return this.defaultEstimate;
return Math.min(this.fast.value, this.slow.value);
}
} defaultEstimate হলো প্রথম সেগমেন্টের আগে, যখন কোনো মাপ নেই, তখনকার অনুমান। hls.js-এ এটা abrEwmaDefaultEstimate (৫ লাখ bps), Shaka-তে abr.defaultBandwidthEstimate (একই ৫ লাখ)। আগের সেশনের মাপা থ্রুপুট localStorage-এ রেখে পরেরবার এখানে বসালে স্টার্টআপ অনেক ভালো হয় (পরের চ্যাপ্টারে আরো আছে)।
উপরে ওঠা কঠিন, নিচে নামা সহজ
ABR-এর সবচেয়ে গুরুত্বপূর্ণ নিয়ম হলো অসমতা: ভুল করে উপরে ওঠার শাস্তি রিবাফার, ভুল করে নিচে থাকার শাস্তি একটু ঝাপসা ছবি। দর্শক দ্বিতীয়টা অনেক বেশি ক্ষমা করে। তাই সব লাইব্রেরি উপরে ওঠার শর্ত কড়া রাখে:
| লাইব্রেরি | থাকা বা নামার শর্ত | উপরে ওঠার শর্ত | সুইচের মাঝে ন্যূনতম বিরতি |
|---|---|---|---|
| hls.js | abrBandWidthFactor ০.৯৫ × অনুমান > বিটরেট | abrBandWidthUpFactor ০.৭ × অনুমান > বিটরেট | abrSwitchInterval (০) |
| Shaka | abr.bandwidthDowngradeTarget ০.৯৫ | abr.bandwidthUpgradeTarget ০.৮৫ | abr.switchInterval ৮s |
মানে hls.js-এ ৫ Mbps অনুমানে ৩.৫ Mbps পর্যন্ত রেন্ডিশনে উঠবে, কিন্তু ইতিমধ্যে ৪.৭ Mbps-এ থাকলে সেখানেই থাকবে। এই ফাঁকটুকুই হিস্টেরেসিস, যা দুই রেন্ডিশনের মাঝে দোলা থামায়।
জরুরি নিচে নামা
hls.js আরেকটা কাজ করে যা সহজে চোখে পড়ে না: একটা সেগমেন্ট নামানোর মাঝপথে সে হিসাব করে, এই গতিতে চললে সেগমেন্টটা কি বাফার শেষ হওয়ার আগে পৌঁছাবে? না পৌঁছালে সে ডাউনলোড বাতিল করে নিচের রেন্ডিশনের সেগমেন্ট আনে। maxStarvationDelay আর maxLoadingDelay (দুটোই ডিফল্ট ৪ সেকেন্ড) এই সিদ্ধান্তের সীমা ঠিক করে। হঠাৎ নেটওয়ার্ক পড়ে গেলে (লিফটে ঢোকা, মেট্রোর টানেল) এটাই রিবাফার বাঁচায়।
BOLA আর হাইব্রিড, লাইব্রেরিতে
dash.js-এ দুটো ABR রুল আছে, throughputRule আর bolaRule। ভার্সন ৫ থেকে প্রতিটা আলাদা করে চালু বা বন্ধ করা যায়, আর দুটোই চালু থাকলে dash.js নিজে “ডায়নামিক” মোডে চলে: শুরুতে থ্রুপুট রুল (কারণ বাফার খালি, BOLA-র কিছু বলার নেই), বাফার টার্গেটে পৌঁছালে BOLA।
player.updateSettings({
streaming: {
abr: {
rules: {
throughputRule: { active: true },
bolaRule: { active: true },
},
},
},
}); dash.js ভার্সন ৪ থেকে ৫-এ যাওয়ার সময় ABRStrategy সেটিং মুছে ফেলা হয়েছে, আর stableBufferTime-এর নাম হয়েছে bufferTimeDefault। পুরনো ব্লগ পোস্ট থেকে কনফিগ কপি করলে সেটিং নীরবে কাজ করবে না, কোনো এরর ছাড়াই। এটা dash.js-এর মাইগ্রেশন গাইডে আছে।
hls.js আর Shaka মূলত থ্রুপুট-ভিত্তিক, কিন্তু বাফার লেভেলকে নিরাপত্তার সীমা হিসেবে ব্যবহার করে (উপরের জরুরি নামা তারই উদাহরণ)। কোন পদ্ধতি ভালো তার কোনো সার্বজনীন উত্তর নেই; উত্তর আসে আপনার দর্শকের আসল ডেটা থেকে (চ্যাপ্টার ১১)।
ডিভাইসের সীমায় ক্যাপ করা
নেটওয়ার্ক যত ভালোই হোক, তিনটা কারণে উঁচু রেন্ডিশন অপচয়:
ভিউপোর্ট। ৩৬০ পিক্সেল চওড়া প্লেয়ারে 1080p নামানো মানে চারগুণের বেশি ডেটা, কোনো দৃশ্যমান লাভ ছাড়া। hls.js-এ capLevelToPlayerSize: true প্লেয়ারের আকার (ডিভাইস পিক্সেল রেশিও সহ) দেখে উপরের রেন্ডিশন আটকায়। ডিফল্টে এটা বন্ধ, তাই চালু করতে হয়। ফুলস্ক্রিনে গেলে সে আবার উপরে উঠতে দেয়।
ফ্রেম ড্রপ। দুর্বল ফোন বা পুরনো ল্যাপটপ 1080p60 ডিকোড করতে না পারলে ফ্রেম ফেলে দেয়, ছবি থেমে থেমে চলে, অথচ নেটওয়ার্ক মাপ বলছে সব ঠিক। capLevelOnFPSDrop: true (ডিফল্ট বন্ধ) ফ্রেম ড্রপের হার একটা সীমা পেরোলে (fpsDroppedMonitoringThreshold ০.২, প্রতি fpsDroppedMonitoringPeriod ৫ সেকেন্ডে মাপা) রেন্ডিশন নামায়। নিজে মাপতে চাইলে:
setInterval(() => {
const q = video.getVideoPlaybackQuality();
const ratio = q.totalVideoFrames ? q.droppedVideoFrames / q.totalVideoFrames : 0;
console.log(`dropped ${q.droppedVideoFrames}/${q.totalVideoFrames} = ${(ratio * 100).toFixed(1)}%`);
}, 5000); dropped 3/1450 = 0.2% <- healthy
dropped 412/1720 = 24.0% <- the device cannot keep up with this rendition কোডেক সাপোর্ট। এটা ABR-এর আগের ধাপ: ডিভাইস যে কোডেক চালাতে পারে না (যেমন পুরনো ডিভাইসে HEVC বা AV1), সেই রেন্ডিশন তালিকা থেকেই বাদ যায়। এটা পরীক্ষা হয় MediaSource.isTypeSupported() বা navigator.mediaCapabilities.decodingInfo() দিয়ে, যা আছে MSE চ্যাপ্টারে।
স্টল, ফাঁক আর নাজ
বাফারে পর্যাপ্ত ডেটা আছে, অথচ ভিডিও থেমে আছে। এটা প্লেয়ারের সবচেয়ে বিভ্রান্তিকর সমস্যা, আর এর প্রায় সবসময় কারণ বাফারের ফাঁক: video.buffered-এ দুটো রেঞ্জ, মাঝে কয়েক মিলিসেকেন্ড বা সেকেন্ডের খালি জায়গা, আর প্লেহেড ঠিক সেই ফাঁকের শুরুতে দাঁড়িয়ে।
ফাঁক তৈরি হয় যেভাবে:
- দুই রেন্ডিশনের সেগমেন্টের টাইমস্ট্যাম্প পুরোপুরি মেলে না, সুইচের জায়গায় কয়েক মিলিসেকেন্ড ফাঁক।
- অডিও আর ভিডিওর শুরু একটু আলাদা (এনকোডারের প্রাইমিং স্যাম্পল)।
- একটা সেগমেন্ট নামাতে ব্যর্থ হয়ে প্লেয়ার পরেরটায় চলে গেছে।
- কনটেন্টে সত্যিই একটা ফাঁক আছে (খারাপ এনকোড, লাইভ এনকোডার রিস্টার্ট)।
video.buffered:
range 0: 0.000 -> 31.980
range 1: 32.040 -> 61.900
currentTime: 31.980 readyState: 2 (HAVE_CURRENT_DATA) paused: false ব্রাউজার নিজে ছোট ফাঁক পার হয় না। তাই সব প্লেয়ার একটা “গ্যাপ কন্ট্রোলার” চালায়। hls.js-এ নিয়মগুলো এরকম (ডকস অনুযায়ী):
maxBufferHole(০.১ সেকেন্ড) পর্যন্ত ফাঁক সে স্বাভাবিক ধরে নেয়, পরের সেগমেন্ট খোঁজার সময় এটুকু ছাড় দেয়।- প্লেহেড আটকে থাকলে সে
nudgeOffset(০.১ সেকেন্ড) পরিমাণ সামনে ঠেলে দেয়। nudgeMaxRetry(৩) বার চেষ্টার পরও না চললে একটা ফেটালBUFFER_STALLED_ERRORছোড়ে; ফাঁক লাফাতে গিয়ে বারবার ব্যর্থ হলেBUFFER_SEEK_OVER_HOLE।
Shaka-র ডিজাইন ডকুমেন্টও একই কথা বলে: স্ট্রিমিং ইঞ্জিন ফাঁকের কথা জানে না, প্লেহেড লেয়ার ফাঁক দেখলে লাফ দেয়।
নিজের প্লেয়ারে একটা ন্যূনতম গ্যাপ জাম্পার:
const MAX_JUMP = 0.5; // seconds; larger gaps usually mean a real content problem
function rangeAhead(buffered, t) {
for (let i = 0; i < buffered.length; i++) {
if (buffered.start(i) > t) return buffered.start(i);
}
return null;
}
let lastTime = -1;
setInterval(() => {
if (video.paused || video.seeking || video.ended) return;
if (video.currentTime !== lastTime) { lastTime = video.currentTime; return; }
const next = rangeAhead(video.buffered, video.currentTime);
if (next !== null && next - video.currentTime <= MAX_JUMP) {
console.warn(`stalled at ${video.currentTime.toFixed(3)}, jumping to ${next.toFixed(3)}`);
video.currentTime = next + 0.01;
}
}, 250); TV আর দুর্বল ডিভাইসের মেমরি
ডেস্কটপ ব্রাউজারে SourceBuffer-এর সীমা বিশাল, কিন্তু স্মার্ট TV, স্ট্রিমিং স্টিক বা পুরনো ফোনে অনেক ছোট, আর নির্মাতাভেদে আলাদা। নির্দিষ্ট সংখ্যা কোথাও প্রকাশিত নেই, তাই আন্দাজ না করে মেপে নিন। লক্ষণ একটাই: appendBuffer থেকে QuotaExceededError। hls.js আর Shaka এই এরর পেলে পেছনের বাফার মুছে আবার চেষ্টা করে, কিন্তু সামনের বাফার গোল যদি নিজেই সীমার বেশি হয়, চক্রটা চলতেই থাকে।
TV প্রোফাইলের জন্য একটা রক্ষণশীল শুরু:
const isTv = /SMART-TV|Tizen|Web0S|HbbTV/i.test(navigator.userAgent);
const hls = new Hls(
isTv
? {
maxBufferLength: 20,
maxMaxBufferLength: 30,
maxBufferSize: 20 * 1000 * 1000,
backBufferLength: 10,
}
: { backBufferLength: 90 }
); User-Agent দেখে ডিভাইস চেনা ভঙ্গুর, কিন্তু TV-তে এর চেয়ে ভালো উপায় কমই আছে। TV-র বাকি অদ্ভুততা আছে মোবাইল, TV ও কাস্টিং চ্যাপ্টারে।
hls.js টিউন করার একটা উদাহরণ
শর্ট ভিডিওর একটা ফিড, যেখানে বেশিরভাগ দর্শক মোবাইল ডেটায়, আর ভিডিও গড়ে দেড় মিনিট:
const saved = Number(localStorage.getItem('bw-estimate')) || undefined;
const hls = new Hls({
maxBufferLength: 12, // most viewers leave early; don't pay for what they skip
backBufferLength: 30,
capLevelToPlayerSize: true, // a feed card is small
capLevelOnFPSDrop: true,
abrEwmaDefaultEstimate: saved ?? 500_000,
abrBandWidthUpFactor: 0.7, // default, stated so the next reader sees the choice
});
hls.on(Hls.Events.FRAG_LOADED, () => {
try { localStorage.setItem('bw-estimate', String(Math.round(hls.bandwidthEstimate))); } catch {}
}); লম্বা সিনেমার জন্য একই কোড উল্টো দিকে যাবে: maxBufferLength ৬০, ব্যাক বাফার বেশি, আর ক্যাপিং চালু থাকলেও ফুলস্ক্রিন ধরে নেওয়া।
খারাপ নেটওয়ার্ক বানিয়ে পরীক্ষা
আপনার অফিসের ফাইবারে ABR কখনো ভুল করবে না। দর্শকের ফোনে করবে। তাই খারাপ নেটওয়ার্ক তৈরি করে পরীক্ষা করতে হয়।
ব্রাউজারে: Chrome DevTools-এর Network প্যানেলে থ্রটলিং ড্রপডাউন থেকে প্রিসেট প্রোফাইল, অথবা “Add custom profile” দিয়ে নিজের ডাউনলোড গতি, আপলোড গতি আর লেটেন্সি। সীমাবদ্ধতা: এটা প্রতি রিকোয়েস্টে স্থির দেরি যোগ করে, প্যাকেট লস বা হঠাৎ গতি পড়ে যাওয়া অনুকরণ করে না।
লিনাক্সে, আসল প্যাকেট লেভেলে: tc netem দিয়ে একটা ইন্টারফেসে দেরি, জিটার, লস আর গতির সীমা বসানো যায়। প্লেয়ার একটা লিনাক্স মেশিনে চালিয়ে, বা একটা লিনাক্স রাউটার দিয়ে ফোনের ট্রাফিক পাঠিয়ে:
# 2 Mbit/s, 120 ms delay with 30 ms jitter, 1% loss
sudo tc qdisc add dev eth0 root netem rate 2mbit delay 120ms 30ms loss 1%
# mid-test: the train enters a tunnel
sudo tc qdisc change dev eth0 root netem rate 300kbit delay 400ms 100ms loss 5%
# restore
sudo tc qdisc del dev eth0 root একটা ভালো পরীক্ষার স্ক্রিপ্ট: ২ মিনিট ভালো নেটওয়ার্ক, তারপর ৩০ সেকেন্ড টানেল, তারপর আবার ভালো। প্রতিটা ধাপে দেখুন প্লেয়ার কত দ্রুত নামল, রিবাফার হলো কিনা, আর ফিরে উঠতে কত সময় নিল। hls.js-এ debug: true দিলে প্রতিটা সিদ্ধান্ত কনসোলে আসে; নিজের লগের জন্য ইভেন্ট শুনুন:
hls.on(Hls.Events.LEVEL_SWITCHED, (_, d) => {
const l = hls.levels[d.level];
console.log(`${video.currentTime.toFixed(1)}s -> ${l.height}p @ ${(l.bitrate / 1e6).toFixed(2)} Mbps, est ${(hls.bandwidthEstimate / 1e6).toFixed(2)} Mbps`);
}); 12.0s -> 720p @ 2.80 Mbps, est 4.91 Mbps
130.4s -> 480p @ 1.40 Mbps, est 1.62 Mbps
134.4s -> 240p @ 0.40 Mbps, est 0.38 Mbps
171.9s -> 480p @ 1.40 Mbps, est 2.25 Mbps
184.0s -> 720p @ 2.80 Mbps, est 4.40 Mbps (আউটপুটটা উদাহরণ।) এখানে লক্ষ করুন নামতে লাগল ৪ সেকেন্ড, কিন্তু ফিরে 720p-তে উঠতে ১২ সেকেন্ড: এটাই ধীর EWMA আর কড়া আপ-ফ্যাক্টরের ইচ্ছাকৃত ফল।
ডিবাগিং: এই চ্যাপ্টারের সমস্যাগুলো
| লক্ষণ | কীভাবে নিশ্চিত হবেন | সমাধান |
|---|---|---|
| ভালো নেটওয়ার্কেও প্লেয়ার নিচু রেন্ডিশনে আটকে | LEVEL_SWITCHED লগ আর hls.bandwidthEstimate; DevTools Network-এ সেগমেন্টের সাইজ ও টাইমিং | সেগমেন্ট খুব ছোট বা TTFB বড় (CDN miss)। সেগমেন্ট দৈর্ঘ্য বাড়ান, CDN হিট রেশিও দেখুন, abrEwmaDefaultEstimate আগের সেশন থেকে বসান |
| দুই রেন্ডিশনের মাঝে বারবার দোলা | লগে কাছাকাছি সময়ে উপরে-নিচে সুইচ | ল্যাডারে দুই ধাপের বিটরেট খুব কাছাকাছি; ধাপ ছড়ান বা abrSwitchInterval / switchInterval বাড়ান |
| হঠাৎ খুব উঁচু রেন্ডিশন, তারপর রিবাফার | এস্টিমেট এক লাফে অনেক বেড়েছে; ওই সেগমেন্টের Network টাইমিং প্রায় শূন্য | ক্যাশ বা সার্ভিস ওয়ার্কার থেকে আসা সেগমেন্ট থ্রুপুটে গোনা হচ্ছে; কাস্টম লোডারে এমন নমুনা বাদ দিন |
| বাফারে ডেটা আছে কিন্তু ভিডিও থেমে | কনসোলে video.buffered প্রতিটা রেঞ্জ আর currentTime প্রিন্ট | ফাঁক আছে। ছোট হলে প্লেয়ারের গ্যাপ জাম্পিং চালু আছে কিনা দেখুন; বড় হলে এনকোড বা প্যাকেজিংয়ে টাইমস্ট্যাম্প ঠিক করুন |
hls.js-এ BUFFER_STALLED_ERROR ফেটাল | hls.on(Hls.Events.ERROR) লগে details | নাজ তিনবার ব্যর্থ। সাধারণত কনটেন্টে সত্যিকারের ফাঁক বা ডিকোডার আটকে গেছে; hls.recoverMediaError() চেষ্টা করুন, তারপর সোর্স ঠিক করুন |
| TV-তে ১৫-২০ মিনিট পর প্লেব্যাক ভাঙে | এরর লগে QuotaExceededError বা BUFFER_FULL_ERROR | backBufferLength আর maxBufferSize কমান |
| ফোনে ছবি থেমে থেমে চলে, কিন্তু রিবাফার নেই | getVideoPlaybackQuality()-এ ড্রপ হার বেশি | capLevelOnFPSDrop চালু করুন, অথবা দুর্বল ডিভাইসে উঁচু রেন্ডিশন বন্ধ করুন |
| ছোট প্লেয়ারে 1080p নামছে | Network-এ সেগমেন্ট পাথে রেজোলিউশন | capLevelToPlayerSize: true |
| পুরনো ব্লগের dash.js কনফিগ কাজ করে না | player.getSettings() প্রিন্ট করে দেখুন সেটিং বসেছে কিনা | ভার্সন ৫-এ নাম বদলেছে; মাইগ্রেশন গাইড দেখে নতুন নাম দিন |
মূল শিক্ষা
- বাফার তিনটা সীমা: সামনে কতটা, চালু করতে কতটা, পেছনে কতটা। hls.js সামনে সেকেন্ড নয় বাইট ধরে (৬০ MB), আর ডিফল্টে পেছনের বাফার নিজে মোছে না।
- বাফার গোল ব্যবসার সিদ্ধান্ত: শর্ট ভিডিওতে কম (অপচয় কমে), লম্বা কনটেন্টে বেশি (রিবাফার কমে)।
- থ্রুপুটের মাপ নিজেই দূষিত: ছোট সেগমেন্ট কম দেখায়, ক্যাশ হিট ভুয়া বেশি দেখায়, বাতিল ডাউনলোড অর্থহীন। এগুলো ফিল্টার না করলে কোনো অ্যালগরিদম বাঁচাতে পারবে না।
- দুই EWMA-র মিনিমাম, মিডিয়া সেকেন্ডে ওজন দেওয়া: দ্রুত পতন ধরে, ধীরে ওঠে।
- উপরে ওঠা কঠিন, নিচে নামা সহজ: hls.js-এ ০.৭ বনাম ০.৯৫, Shaka-তে ০.৮৫ বনাম ০.৯৫। এই ফাঁকটাই দোলা থামায়।
- ডিভাইসের সীমায় ক্যাপ করুন: ভিউপোর্ট, ফ্রেম ড্রপ আর কোডেক। hls.js-এ দুটো ক্যাপিং-ই ডিফল্টে বন্ধ।
- বাফারে ডেটা থাকা সত্ত্বেও স্টল মানে প্রায় সবসময় ফাঁক:
video.buffered-এর প্রতিটা রেঞ্জ দেখুন, শুধু প্রথমটা নয়। - TV-র মেমরি ছোট আর অপ্রকাশিত:
QuotaExceededErrorদেখলে বাফার গোল আর ব্যাক বাফার কমান। - খারাপ নেটওয়ার্ক বানিয়ে পরীক্ষা করুন: DevTools থ্রটলিং দ্রুত,
tc netemআসলের কাছাকাছি।