স্টার্টআপ, সিক ও থাম্বনেইল
প্লে চাপা থেকে প্রথম ফ্রেম পর্যন্ত প্রতিটা মিলিসেকেন্ড কোথায় যায়, কীভাবে কমাবেন, ফিডে পরের ভিডিও আগে থেকে তৈরি রাখা, কীফ্রেমে সিক, আর টাইমলাইনের থাম্বনেইল: WebVTT স্প্রাইট, I-frame প্লেলিস্ট, DASH ইমেজ ট্র্যাক।
গল্পে বুঝি
কিন্দি কমলাপুর স্টেশন থেকে চট্টগ্রাম যাবে। ট্রেন ছাড়ার পাঁচ মিনিট আগে সে পৌঁছাল। গেট দিয়ে ঢোকা, টিকিট কোন প্ল্যাটফর্মের সেটা বোর্ডে খোঁজা, টিকিট চেকারের কাছে দেখানো, তারপর ঠিক বগিটা খুঁজে উঠে বসা। প্রতিটা ধাপ ছোট, কিন্তু ধাপগুলো একটার পর একটা, আর যেকোনো একটায় আটকালে পুরো যাত্রা দেরি। তার বন্ধু ফারাবি চালাক: সে আগের দিন অনলাইনে টিকিট কেটে প্ল্যাটফর্ম নম্বর জেনে রেখেছে, স্টেশনে ঢুকেই সোজা বগিতে। ঠিক সময়ে দুজনেই ট্রেনে, কিন্তু ফারাবির হাতে চা খাওয়ার সময় ছিল।
একবার আন্তঃনগর ট্রেন দেরি করছিল। কিন্দি অপেক্ষা না করে সামনের লোকাল ট্রেনে উঠে পড়ল, টঙ্গী গিয়ে আন্তঃনগরে বদলে নিল। লোকাল ধীর আর ভিড়, কিন্তু সে অন্তত রওনা দিয়েছে। আরেকবার সে ভৈরবে নামতে চাইল, পথের মাঝখানে। কিন্তু ট্রেন যেখানে খুশি থামে না, থামে শুধু স্টেশনে। ভৈরবের আগে স্টেশন না থাকলে তাকে আগের স্টেশনে নেমে বাকিটা হেঁটে বা রিকশায় যেতে হয়, আর স্টেশনগুলো যত দূরে দূরে, হাঁটা তত বেশি। পথে জানালার পাশে মরিয়ম একটা ছোট বই বের করল: প্রতিটা স্টেশনের একটা করে ছোট ছবি, একটা পাতায় সাজানো। কোথায় নামবে ঠিক করার আগে ট্রেন না থামিয়েই ছবি দেখে বুঝে নেওয়া যায় কোন স্টেশন কেমন।
মিলিয়ে নিই: গেট, বোর্ড, চেকার আর বগি খোঁজা হলো স্টার্টআপের ধাপগুলো: DNS, TLS, ম্যানিফেস্ট, init সেগমেন্ট, প্রথম মিডিয়া সেগমেন্ট, ডিকোড। প্রতিটা ক্রমিক, তাই যোগ হয়। ফারাবির আগে থেকে টিকিট ও প্ল্যাটফর্ম জেনে রাখা হলো preconnect আর প্রিলোড, যা ধাপগুলোকে প্লে চাপার আগেই সেরে রাখে। লোকাল ট্রেনে রওনা দিয়ে পরে বদলানো হলো নিচু রেন্ডিশনে শুরু করে উপরে ওঠা। ট্রেন শুধু স্টেশনে থামা হলো সিক শুধু কীফ্রেম থেকে ডিকোড শুরু করতে পারা, আর আগের স্টেশন থেকে হাঁটা হলো নির্ভুল সিকের ডিকোড খরচ, যা GOP যত লম্বা তত বেশি। মরিয়মের ছবির বই হলো থাম্বনেইল স্প্রাইট: টাইমলাইন ধরে টানার সময় আসল ভিডিও না নামিয়েই কোথায় কী আছে দেখানো। বাস্তবে YouTube, Netflix থেকে যেকোনো নিউজ সাইটের প্লেয়ার এই চারটা কৌশল দিয়েই দ্রুত শুরু আর মসৃণ সিক দেয়।
এই চ্যাপ্টার কোথা থেকে শুরু করে
স্টার্টআপ কেন সবচেয়ে গুরুত্বপূর্ণ QoE মেট্রিক, প্রথম সিদ্ধান্ত তথ্য ছাড়া নেওয়ার সমস্যা, আর সিকের মূল ধাপগুলো আছে প্লেব্যাক ও ABR চ্যাপ্টারে। GOP আর কীফ্রেম কী, সেটা কোডেক চ্যাপ্টারে। এখানে আমরা মাপব, ভাঙব, আর প্রতিটা ধাপ কমানোর বাস্তব কোড দেখব।
টাইম-টু-ফার্স্ট-ফ্রেম ভেঙে দেখা
ঠান্ডা অবস্থায় (কোনো কানেকশন খোলা নেই, কিছু ক্যাশে নেই) একটা HLS ভিডিও চালাতে যা যা ক্রমিকভাবে ঘটে:
| ধাপ | কী হয় | ঠান্ডা অবস্থায় RTT লাগে |
|---|---|---|
| ১. প্লেয়ার JS লোড | hls.js বা Shaka নিজেই একটা ডাউনলোড | পেজের সাথে |
| ২. DNS | CDN হোস্টনেম রিজলভ | ১ (রিজলভার ক্যাশে না থাকলে বেশি) |
| ৩. TCP + TLS | কানেকশন খোলা | TLS 1.3-এ মোটামুটি ২ |
| ৪. মাল্টিভেরিয়েন্ট প্লেলিস্ট | কোন কোন রেন্ডিশন আছে | ১ + ডাউনলোড |
| ৫. মিডিয়া প্লেলিস্ট | ওই রেন্ডিশনের সেগমেন্ট তালিকা | ১ + ডাউনলোড |
| ৬. init সেগমেন্ট | কোডেক কনফিগ (fMP4) | ১ + ডাউনলোড |
| ৭. প্রথম মিডিয়া সেগমেন্ট | আসল ছবি | ১ + ডাউনলোড |
| ৮. DRM লাইসেন্স | এনক্রিপ্টেড হলে | ১ বা তার বেশি, অন্য সার্ভারে |
| ৯. ডিকোড ও রেন্ডার | প্রথম ফ্রেম স্ক্রিনে | ডিভাইসের উপর |
ঢাকা থেকে সিঙ্গাপুরের কোনো এজে RTT যদি ৫০ মিলিসেকেন্ড হয়, শুধু রাউন্ড ট্রিপেই আট-নয়টা, মানে আধা সেকেন্ডের কাছাকাছি, ডাউনলোড সময় ছাড়াই। এজ ঢাকায় থাকলে এটা অনেক কমে; এজ দূরে হলে অনেক বাড়ে। এজন্যই CDN-এর রিকোয়েস্ট যাত্রা এখানে সরাসরি দর্শকের অভিজ্ঞতা।
নিজে মাপুন
ব্রাউজারের Resource Timing API প্রতিটা রিকোয়েস্টের ধাপ আলাদা দেয়, আর requestVideoFrameCallback আসল প্রথম ফ্রেমের মুহূর্ত দেয় (canplay বা playing ইভেন্টের চেয়ে নির্ভুল; বেশিরভাগ আধুনিক ব্রাউজারে আছে, না থাকলে playing-এ ফিরে যান):
const t0 = performance.now();
video.play();
const onFirstFrame = () => {
const ttff = performance.now() - t0;
const rows = performance.getEntriesByType('resource')
.filter((e) => /\.(m3u8|mp4|m4s|ts)(\?|$)/.test(e.name))
.map((e) => ({
file: e.name.split('/').pop().slice(0, 28),
dns: Math.round(e.domainLookupEnd - e.domainLookupStart),
connect: Math.round(e.connectEnd - e.connectStart),
ttfb: Math.round(e.responseStart - e.requestStart),
body: Math.round(e.responseEnd - e.responseStart),
startedAt: Math.round(e.startTime - t0),
}));
console.table(rows);
console.log(`time to first frame: ${Math.round(ttff)} ms`);
};
'requestVideoFrameCallback' in video
? video.requestVideoFrameCallback(onFirstFrame)
: video.addEventListener('playing', onFirstFrame, { once: true }); ┌─────────┬──────────────────────┬─────┬─────────┬──────┬──────┬───────────┐
│ (index) │ file │ dns │ connect │ ttfb │ body │ startedAt │
├─────────┼──────────────────────┼─────┼─────────┼──────┼──────┼───────────┤
│ 0 │ master.m3u8 │ 38 │ 104 │ 61 │ 2 │ 4 │
│ 1 │ 720p.m3u8 │ 0 │ 0 │ 58 │ 1 │ 214 │
│ 2 │ init.mp4 │ 0 │ 0 │ 55 │ 1 │ 281 │
│ 3 │ seg_00000.m4s │ 0 │ 0 │ 63 │ 212 │ 343 │
└─────────┴──────────────────────┴─────┴─────────┴──────┴──────┴───────────┘
time to first frame: 701 ms (আউটপুটটা উদাহরণ।) টেবিলটা পড়ার নিয়ম: startedAt কলামে প্রতিটা রিকোয়েস্ট আগেরটা শেষ হওয়ার পরে শুরু হয়েছে কিনা দেখুন। এখানে চারটাই সিরিয়াল, আর সবচেয়ে বড় একক খরচ প্রথম সেগমেন্টের body (২১২ ms)। Cross-origin CDN থেকে আসা রিকোয়েস্টে dns, connect, ttfb শূন্য দেখালে CDN-কে Timing-Allow-Origin হেডার পাঠাতে হবে, নইলে ব্রাউজার বিস্তারিত লুকায়।
প্রতিটা ধাপ কমানো
১. কানেকশন আগেই খুলে রাখা
প্লেয়ার পেজে থাকলেই CDN-এর সাথে কানেকশন খুলুন, ইউজার প্লে চাপার আগেই। এতে DNS আর TCP+TLS (উপরের ২ ও ৩ নম্বর) টেবিল থেকে বাদ পড়ে:
<link rel="preconnect" href="https://media.baytalhikma.org" crossorigin> crossorigin অ্যাট্রিবিউট দরকার যদি সেগমেন্টগুলো CORS মোডে আনা হয় (MSE প্লেয়ার fetch দিয়ে আনে, তাই প্রায় সবসময়), নইলে ব্রাউজার একটা আলাদা কানেকশন খোলে আর preconnect-টা কাজে লাগে না।
২. ম্যানিফেস্ট আগে থেকে আনা
মাল্টিভেরিয়েন্ট প্লেলিস্ট ছোট, তাই পেজ লোডের সময়ই আনা যায়। hls.js-এ autoStartLoad: false দিয়ে প্লেয়ার তৈরি করে loadSource ডাকলে সে ম্যানিফেস্ট আনে, কিন্তু সেগমেন্ট নামানো শুরু করে না যতক্ষণ না আপনি startLoad() ডাকেন:
const hls = new Hls({ autoStartLoad: false });
hls.loadSource('https://media.baytalhikma.org/v/7f3a/master.m3u8');
hls.attachMedia(video);
playButton.addEventListener('click', () => {
hls.startLoad(0);
video.play();
}, { once: true }); ৩. প্রথম রেন্ডিশন বেছে নেওয়া
hls.js-এর ডিফল্ট আচরণ অনেকে জানে না: startLevel সেট না করলে সে ম্যানিফেস্টে প্রথম যে ভেরিয়েন্ট লেখা আছে সেটা দিয়ে শুরু করে (ডকস অনুযায়ী ডিফল্ট hls.firstLevel)। তাই প্যাকেজার মাল্টিভেরিয়েন্ট প্লেলিস্টে কোন রেন্ডিশন আগে লেখে, সেটা সরাসরি আপনার স্টার্টআপ ঠিক করে। Safari-র নেটিভ HLS-ও প্রথম তালিকাভুক্ত ভেরিয়েন্ট দিয়ে শুরু করে বলে Apple-এর অথরিং গাইডলাইন ধরে নেয়। ঢাকার মোবাইল দর্শকের জন্য মাঝারি একটা রেন্ডিশন (ধরুন 480p বা 540p) প্রথমে রাখা প্রায়ই সবচেয়ে ভালো আপস।
বিকল্প হলো startLevel: -1: তখন hls.js সবচেয়ে নিচু লেভেলের একটা সেগমেন্ট নামিয়ে থ্রুপুট মাপে (testBandwidth, ডিফল্ট চালু), তারপর বাছে। এতে সিদ্ধান্ত ভালো হয়, কিন্তু একটা বাড়তি ডাউনলোড যোগ হয়। আগের সেশনের মাপা থ্রুপুট থাকলে সবচেয়ে ভালো: সেটা abrEwmaDefaultEstimate-এ বসিয়ে testBandwidth: false (আগের চ্যাপ্টারের কোড দেখুন)।
৪. প্রথম সেগমেন্ট ছোট রাখা
প্লেব্যাক শুরু করতে পুরো বাফার গোল লাগে না, একটা সেগমেন্টই যথেষ্ট। ৬ সেকেন্ডের সেগমেন্ট নামাতে যতক্ষণ লাগে, ২ সেকেন্ডের সেগমেন্টে তার প্রায় তিন ভাগের এক ভাগ। কিছু প্ল্যাটফর্ম শুধু প্রথম কয়েকটা সেগমেন্ট ছোট করে প্যাকেজ করে, বাকিগুলো স্বাভাবিক দৈর্ঘ্যে। এর ট্রেডঅফ (ছোট সেগমেন্টে বেশি রিকোয়েস্ট, কম্প্রেশনে ক্ষতি) আছে প্যাকেজিং চ্যাপ্টারে।
৫. পোস্টার আর প্লেসহোল্ডার
যতক্ষণ প্রথম ফ্রেম আসেনি, স্ক্রিনে কিছু থাকা চাই। poster অ্যাট্রিবিউটে ভিডিওর প্রথম ফ্রেমের মতো দেখতে একটা ছোট, ভালোভাবে কম্প্রেস করা ছবি দিলে দর্শকের চোখে লোডিং প্রায় অদৃশ্য হয়ে যায়, কারণ প্রথম ফ্রেম এলে ছবিটা প্রায় বদলায় না। আর প্লেয়ারের বক্সের মাপ (aspect-ratio CSS) আগে থেকে ঠিক রাখুন, নইলে পেজ লাফায় (CLS)।
৬. DRM লাইসেন্স সমান্তরালে
এনক্রিপ্টেড কনটেন্টে লাইসেন্স রিকোয়েস্ট প্রথম সেগমেন্টের সাথে সমান্তরালে যাওয়া উচিত, পরে নয়। এর বিস্তারিত DRM ও EME চ্যাপ্টারে।
ফিডে পরের ভিডিও আগে থেকে তৈরি রাখা
শর্ট ভিডিওর ফিডে (রিল, শর্টস) স্ক্রল করার সাথে সাথে পরের ভিডিও চলা দরকার, প্রায় শূন্য অপেক্ষায়। কৌশলটা: যে কার্ডটা পরে আসবে সেটার প্লেয়ার আগে তৈরি করে ম্যানিফেস্ট আর প্রথম সেগমেন্ট নামিয়ে রাখা, কিন্তু চালু না করা।
const MAX_LIVE_PLAYERS = 3; // current + next + previous; more costs memory and decoders
function prepare(card) {
if (card.hls) return;
const hls = new Hls({ autoStartLoad: false, maxBufferLength: 4 });
hls.loadSource(card.dataset.src);
hls.attachMedia(card.querySelector('video'));
hls.once(Hls.Events.MANIFEST_PARSED, () => hls.startLoad(0)); // fetch the first segment, then stop at 4 s
card.hls = hls;
}
function release(card) {
card.hls?.destroy();
card.hls = null;
}
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
const card = e.target;
if (e.isIntersecting) {
prepare(card);
prepare(card.nextElementSibling ?? card);
} else if (!isNear(card)) {
release(card);
}
}
}, { rootMargin: '100% 0px' }); দুটো সীমা মনে রাখুন। ডিকোডার সীমিত: অনেক মোবাইল ডিভাইসে একসাথে কয়টা হার্ডওয়্যার ভিডিও ডিকোডার চলবে তার একটা সীমা আছে, যা প্রকাশিত নয় আর ডিভাইসভেদে আলাদা; সীমা পেরোলে নতুন ভিডিও কালো থাকে বা সফটওয়্যার ডিকোডে যায়। তাই একসাথে বেশি প্লেয়ার রাখবেন না, আর দূরের কার্ডে destroy() ডাকুন। ডেটা খরচ: যে ভিডিও দর্শক কখনো দেখবে না তার প্রথম সেগমেন্টও তার মোবাইল ডেটা খায়। navigator.connection.saveData সত্য হলে প্রিলোড বন্ধ রাখা ভদ্রতা।
MSE ছাড়া সাধারণ video এলিমেন্টে preload অ্যাট্রিবিউট (none, metadata, auto) একই কাজের একটা মোটা দাগের সংস্করণ, কিন্তু এটা একটা ইঙ্গিত মাত্র: ব্রাউজার নিজের নিয়মে মানতেও পারে, না-ও পারে, বিশেষ করে মোবাইলে।
সিক, ভেতর থেকে
নির্ভুল সিকের খরচ
দর্শক ৪৭:১২-তে সিক করল। ডিকোডার শুধু কীফ্রেম থেকে ডিকোড শুরু করতে পারে, তাই প্লেয়ারকে ৪৭:১২-এর আগের কীফ্রেম থেকে নামিয়ে সেখান থেকে ৪৭:১২ পর্যন্ত সব ফ্রেম ডিকোড করতে হয়, শুধু ফেলে দেওয়ার জন্য। GOP ২ সেকেন্ড হলে গড়ে ১ সেকেন্ডের বাড়তি ডিকোড; GOP ১০ সেকেন্ড হলে গড়ে ৫ সেকেন্ড। দুর্বল ফোনে এই পার্থক্য স্পষ্ট দেখা যায়।
video.currentTime = t সবসময় নির্ভুল সিক। HTML স্ট্যান্ডার্ডে fastSeek(t) নামে একটা দ্রুত বিকল্প আছে যেটা নির্ভুলতা ছেড়ে কাছের কীফ্রেমে যায়, কিন্তু MDN-এর হিসাবে এটা Baseline নয়, সব বড় ব্রাউজারে নেই। তাই ফিচার ডিটেক্ট করে ব্যবহার করুন:
function seekCoarse(t) {
typeof video.fastSeek === 'function' ? video.fastSeek(t) : (video.currentTime = t);
} স্ক্রাবিংয়ের সময় (টাইমলাইন টানার সময়) fastSeek-এর চেয়েও ভালো হলো আদৌ সিক না করা: শুধু থাম্বনেইল দেখান, আর আঙুল ছাড়লে একবার নির্ভুল সিক করুন (নিচের থাম্বনেইল অংশ)।
বাফারের ভেতরে সিক
সিক টার্গেট যদি ইতিমধ্যে video.buffered-এর কোনো রেঞ্জে থাকে, কিছু নামানোর দরকার নেই, সিক প্রায় তাৎক্ষণিক। এজন্যই আগের চ্যাপ্টারের ব্যাক বাফার শুধু মেমরির প্রশ্ন নয়: ১০ সেকেন্ড পিছিয়ে যাওয়ার বাটন (যা মানুষ খুব ব্যবহার করে) ব্যাক বাফার থাকলে ফ্রি, না থাকলে একটা নতুন স্টার্টআপ।
function isBuffered(t) {
for (let i = 0; i < video.buffered.length; i++) {
if (t >= video.buffered.start(i) && t < video.buffered.end(i)) return true;
}
return false;
} সেগমেন্টের সীমানায় পড়া সিক
সিক টার্গেট ঠিক দুই সেগমেন্টের সীমানার কাছে পড়লে কোন সেগমেন্ট নামাতে হবে, সেটা টাইমস্ট্যাম্পের সামান্য গরমিলে ভুল হতে পারে। hls.js এর জন্য maxFragLookUpTolerance (০.২৫ সেকেন্ড) রাখে: সীমানার এত কাছে হলে পরের সেগমেন্ট ধরে নেয়। Shaka-তে streaming.startAtSegmentBoundary চালু করলে প্লেব্যাক সেগমেন্টের শুরুতে বসে, যা কিছু ডিভাইসে সিকের পরের আটকে যাওয়া এড়ায়।
থাম্বনেইল আর ট্রিক প্লে
টাইমলাইনে মাউস রাখলে বা আঙুল দিয়ে টানলে যে ছোট ছবি দেখা যায়, সেটা দেওয়ার তিনটা প্রচলিত উপায়:
| উপায় | কীভাবে কাজ করে | কে বোঝে |
|---|---|---|
| WebVTT + স্প্রাইট | একটা বা কয়েকটা বড় JPEG-এ অনেক ছোট থাম্বনেইল গ্রিডে সাজানো; একটা VTT ফাইলে প্রতিটা সময়-পরিসরের জন্য ছবির নাম আর #xywh= স্থানাঙ্ক | প্রায় সব JS প্লেয়ার UI (Shaka-তে এক্সটার্নাল ট্র্যাক হিসেবে, শুধু VOD) |
| HLS I-frame প্লেলিস্ট | মাল্টিভেরিয়েন্ট প্লেলিস্টে EXT-X-I-FRAME-STREAM-INF, যেটা একটা মিডিয়া প্লেলিস্টে নির্দেশ করে যেখানে EXT-X-I-FRAMES-ONLY আর প্রতিটা এন্ট্রি একটা কীফ্রেমের বাইট-রেঞ্জ | Safari-র নেটিভ প্লেয়ার (ফাস্ট-ফরোয়ার্ড ও স্ক্রাবিং), Shaka |
| DASH ইমেজ অ্যাডাপটেশন সেট | contentType="image" সেট, EssentialProperty দিয়ে টাইল গ্রিডের মাপ জানানো (DASH-IF IOP-এর থাম্বনেইল স্কিম) | Shaka, dash.js |
Shaka-র README অনুযায়ী (২০২৬ সালের অক্টোবর) সে চারটাই বোঝে: DASH-IF ইমেজ সেট, HLS ইমেজ প্লেলিস্ট (EXT-X-IMAGE-STREAM-INF), mjpg কোডেকের I-frame প্লেলিস্ট, আর এক্সটার্নাল WebVTT স্প্রাইট। hls.js নিজে থাম্বনেইল দেখায় না; ওর উপরে বানানো UI লাইব্রেরিগুলো সাধারণত WebVTT স্প্রাইট পড়ে।
সবচেয়ে সহজ আর সবচেয়ে বহনযোগ্য হলো WebVTT স্প্রাইট, তাই সেটাই বানাই।
ffmpeg দিয়ে স্প্রাইট বানানো
প্রতি ৫ সেকেন্ডে একটা ফ্রেম, ১৬০ পিক্সেল চওড়া, ১০×১০ গ্রিডে (প্রতি ছবিতে ১০০টা থাম্বনেইল, মানে ৫০০ সেকেন্ড):
ffmpeg -hide_banner -loglevel error -i lecture.mp4 \
-vf "fps=1/5,scale=160:90:force_original_aspect_ratio=decrease,pad=160:90:(ow-iw)/2:(oh-ih)/2,tile=10x10" \
-q:v 5 sprite_%03d.jpg
ls -lh sprite_*.jpg -rw-r--r-- 1 sina staff 312K sprite_001.jpg
-rw-r--r-- 1 sina staff 298K sprite_002.jpg
-rw-r--r-- 1 sina staff 141K sprite_003.jpg (সাইজ উদাহরণ।) pad থাকায় উল্লম্ব বা চওড়া ভিডিও হলেও প্রতিটা ঘর ঠিক ১৬০×৯০, তাই স্থানাঙ্কের হিসাব সরল থাকে। ১,২৫০ সেকেন্ডের একটা লেকচারে ২৫০টা থাম্বনেইল, তিনটা স্প্রাইট।
VTT ফাইল লেখা
package main
import (
"fmt"
"os"
"strconv"
)
func ts(sec int) string {
return fmt.Sprintf("%02d:%02d:%02d.000", sec/3600, sec/60%60, sec%60)
}
func main() {
duration, _ := strconv.Atoi(os.Args[1]) // seconds, from ffprobe
const every, w, h, cols, rows = 5, 160, 90, 10, 10
perSprite := cols * rows
fmt.Println("WEBVTT")
for i := 0; i*every < duration; i++ {
start, end := i*every, min((i+1)*every, duration)
sheet, cell := i/perSprite+1, i%perSprite
x, y := cell%cols*w, cell/cols*h
fmt.Printf("\n%s --> %s\nsprite_%03d.jpg#xywh=%d,%d,%d,%d\n", ts(start), ts(end), sheet, x, y, w, h)
}
} go run thumbs.go 1250 > thumbs.vtt
head -10 thumbs.vtt; grep -A1 '^00:08:20' thumbs.vtt WEBVTT
00:00:00.000 --> 00:00:05.000
sprite_001.jpg#xywh=0,0,160,90
00:00:05.000 --> 00:00:10.000
sprite_001.jpg#xywh=160,0,160,90
00:00:10.000 --> 00:00:15.000
sprite_001.jpg#xywh=320,0,160,90
00:08:20.000 --> 00:08:25.000
sprite_002.jpg#xywh=0,0,160,90 ৫০০ সেকেন্ডে (০০:০৮:২০) ঠিক দ্বিতীয় স্প্রাইটের প্রথম ঘর, যা প্রত্যাশিত, কারণ প্রতিটা স্প্রাইটে ১০০টা ঘর × ৫ সেকেন্ড। নতুন স্ক্রিপ্টের আউটপুট সবসময় এভাবে একটা পরিচিত সময় ধরে মিলিয়ে নিন।
প্লেয়ারে দেখানো
async function loadThumbs(url) {
const text = await (await fetch(url)).text();
const base = new URL(url);
const toSec = (s) => s.split(':').reduce((a, v) => a * 60 + parseFloat(v), 0);
return text.split(/\n\n+/).slice(1).map((block) => {
const [time, ref] = block.trim().split('\n');
const [from, to] = time.split(' --> ').map(toSec);
const [file, frag] = ref.split('#xywh=');
const [x, y, w, h] = frag.split(',').map(Number);
return { from, to, src: new URL(file, base).href, x, y, w, h };
});
}
function showThumb(cues, t, el) {
const c = cues.find((q) => t >= q.from && t < q.to);
if (!c) return;
Object.assign(el.style, {
width: `${c.w}px`, height: `${c.h}px`,
backgroundImage: `url(${c.src})`,
backgroundPosition: `-${c.x}px -${c.y}px`,
});
} স্প্রাইট JPEG-গুলো প্রথম হোভারেই আনা শুরু করুন (বা link rel="prefetch" দিয়ে), নইলে প্রথম কয়েকবার টানার সময় খালি বাক্স দেখা যায়। আর স্প্রাইট আর VTT, দুটোই লম্বা সময় ক্যাশ করার মতো স্থির ফাইল; ভার্সন URL-এ রাখুন (পার্জ চ্যাপ্টার দেখুন)।
স্প্রাইট ছবির সংখ্যা নিয়ে একটা হিসাব করুন। ১০×১০ গ্রিড মানে প্রতিটা JPEG ৩০০ KB-এর আশেপাশে; ছোট ফোনে দর্শক টাইমলাইনে একবার হাত দিলেই এতটা ডেটা নামে। ৫×৫ গ্রিডে ফাইল ছোট, রিকোয়েস্ট বেশি। মোবাইল-প্রধান দর্শকে ছোট গ্রিড আর ছোট থাম্বনেইল (১২০ পিক্সেল) প্রায়ই ভালো।
ডিবাগিং: এই চ্যাপ্টারের সমস্যাগুলো
| লক্ষণ | কীভাবে নিশ্চিত হবেন | সমাধান |
|---|---|---|
| প্লে চাপার পর অনেকক্ষণ কালো স্ক্রিন | উপরের টাইমিং স্নিপেট; Network প্যানেলের ওয়াটারফল | কোন ধাপ সবচেয়ে বড় দেখুন: connect বড় হলে preconnect, ttfb বড় হলে CDN miss, body বড় হলে প্রথম রেন্ডিশন বা সেগমেন্ট বড় |
| শুরুতেই 1080p নামে আর ধীর নেটওয়ার্কে দেরি | প্রথম সেগমেন্টের পাথ দেখুন; মাল্টিভেরিয়েন্ট প্লেলিস্টের প্রথম ভেরিয়েন্ট দেখুন | hls.js প্রথম তালিকাভুক্ত ভেরিয়েন্টে শুরু করে; মাঝারি রেন্ডিশন প্রথমে রাখুন বা startLevel দিন |
| Resource Timing-এ CDN রিকোয়েস্টের সময় সব শূন্য | কনসোলে performance.getEntriesByType('resource') | CDN রেসপন্সে Timing-Allow-Origin হেডার যোগ করুন |
preconnect দেওয়ার পরও connect সময় আছে | Network প্যানেলে Connection ID কলাম | crossorigin অ্যাট্রিবিউট নেই, ব্রাউজার আলাদা কানেকশন খুলছে |
| ফিডে স্ক্রল করলে কিছু ভিডিও কালো থাকে | একসাথে কয়টা প্লেয়ার সক্রিয় গুনুন; শুধু ফোনে হয় কিনা | হার্ডওয়্যার ডিকোডার শেষ; সক্রিয় প্লেয়ার কমান, দূরের কার্ডে destroy() |
| সিকের পর অনেকক্ষণ ঘুরতে থাকে | সিকের পরের প্রথম সেগমেন্টের রেজোলিউশন আর সময় | সিকে নিচু রেন্ডিশনে নামা বন্ধ, বা GOP অনেক লম্বা; চলতি ডাউনলোড বাতিল হচ্ছে কিনা দেখুন |
| ১০ সেকেন্ড পিছালেও নতুন করে নামায় | video.buffered পেছনের রেঞ্জ আছে কিনা | ব্যাক বাফার শূন্য বা খুব ছোট; backBufferLength বাড়ান |
| থাম্বনেইল ভুল জায়গার দেখায় | একটা পরিচিত সময়ে VTT-র কিউ আর স্প্রাইটের ঘর মিলিয়ে দেখুন | স্প্রাইট বানানোর fps আর VTT-র ইন্টারভাল মেলেনি, বা ঘরের মাপ আলাদা (pad ছাড়া উল্লম্ব ভিডিও) |
| থাম্বনেইল প্রথমবার খালি | Network-এ স্প্রাইট কখন আসছে | হোভারের শুরুতে বা পেজ লোডে স্প্রাইট প্রিফেচ করুন |
| Safari-তে ফাস্ট-ফরোয়ার্ড কাজ করে না | মাল্টিভেরিয়েন্ট প্লেলিস্টে EXT-X-I-FRAME-STREAM-INF আছে কিনা | প্যাকেজারে I-frame প্লেলিস্ট তৈরি চালু করুন |
মূল শিক্ষা
- স্টার্টআপ ক্রমিক ধাপের যোগফল: DNS, TLS, দুই প্লেলিস্ট, init, প্রথম সেগমেন্ট, লাইসেন্স, ডিকোড। প্রতিটা আলাদা মাপুন, আন্দাজে অপ্টিমাইজ করবেন না।
- প্রথম ফ্রেম মাপুন
requestVideoFrameCallbackদিয়ে,canplayদিয়ে নয়। - preconnect আর আগে ম্যানিফেস্ট আনা প্লে চাপার আগেই কয়েকটা রাউন্ড ট্রিপ সেরে রাখে;
crossoriginভুলবেন না। - hls.js মাল্টিভেরিয়েন্ট প্লেলিস্টের প্রথম ভেরিয়েন্টে শুরু করে: প্যাকেজারের ক্রমটাই আপনার স্টার্টআপ নীতি।
- ফিডে পরের ভিডিও আগে তৈরি রাখুন, কিন্তু হার্ডওয়্যার ডিকোডার আর দর্শকের ডেটা সীমিত, তাই দু-তিনটার বেশি নয়।
- নির্ভুল সিকের খরচ GOP-এর সমানুপাতিক;
fastSeekসব ব্রাউজারে নেই, তাই ফিচার ডিটেক্ট করুন। - ব্যাক বাফার মানে ফ্রি পিছানো: ১০ সেকেন্ড পিছানোর বাটন সবচেয়ে বেশি ব্যবহৃত বাটনগুলোর একটা।
- স্ক্রাবিংয়ে সিক নয়, থাম্বনেইল: WebVTT স্প্রাইট সবচেয়ে বহনযোগ্য, I-frame প্লেলিস্ট Safari-র নেটিভ ট্রিক প্লে দেয়, DASH ইমেজ সেট DASH প্লেয়ারে।