Skip to content

ইন্টারঅ্যাক্টিভিটি ও সিঙ্ক

প্রতিটা দর্শক আলাদা লেটেন্সিতে দেখে, তাই "এখন" মানে সবার কাছে আলাদা মুহূর্ত। টাইমড মেটাডেটা, EXT-X-DATERANGE, PROGRAM-DATE-TIME, চ্যাট সিঙ্ক, স্পয়লার সমস্যা, আর ওয়াচ পার্টিতে সবাইকে একসাথে রাখা।

গল্পে বুঝি

পুরান ঢাকার একটা কমিউনিটি সেন্টারে ফারাবির বড় বোনের বিয়ে। মঞ্চে মাইক হাতে উপস্থাপক রুশদ, আর হলটা এত লম্বা যে পেছনের দিকের অতিথিদের জন্য বাইরের প্যান্ডেলে কয়েকটা স্পিকার লাগানো। রুশদ মাইকে বলেন, “এবার বর-কনে মালাবদল করবেন!” সামনের সারির মানুষ সাথে সাথে হাততালি দেন, কিন্তু প্যান্ডেলের অতিথিরা শোনেন একটু পরে, কারণ শব্দ স্পিকারের তার আর লম্বা পথ ঘুরে পৌঁছায়। ফলে হলে হাততালি দুই ঢেউয়ে হয়: প্রথমে সামনে, তারপর পেছনে।

সমস্যাটা আরও জটিল হলো তিন জায়গায়। ফটোগ্রাফার মরিয়ম মালাবদলের ঠিক মুহূর্তটা ধরতে চান, কিন্তু তিনি প্যান্ডেলের দিকে দাঁড়িয়ে, তাই মাইকের ঘোষণা শুনে ক্লিক করলে দেরি হয়ে যায়। ব্যান্ডের দল ঠিক করেছে কনে মঞ্চে ওঠার মুহূর্তে গান শুরু করবে, কিন্তু তারা কোন শব্দের উপর ভরসা করবে, সামনের না পেছনের? আর ফারাবির এক কাজিন সামনে বসে ফোনে প্যান্ডেলের বন্ধুদের মেসেজ পাঠাচ্ছে, “কনে চলে এসেছে!” বন্ধুরা মেসেজটা পায় স্পিকারে ঘোষণা শোনার আগেই। মজাটাই নষ্ট।

ইভেন্ট ম্যানেজার কিন্দি একটা সমাধান বের করলেন। সবার হাতে একটা ছাপানো সময়সূচি: “রাত ৮:৩০:০০ কনের আগমন, ৮:৪২:০০ মালাবদল”। আর মঞ্চের পাশে একটা বড় ডিজিটাল ঘড়ি, প্যান্ডেল থেকেও দেখা যায়। নিয়ম হলো, ঘোষণার শব্দ শুনে নয়, ঘড়ির সময় দেখে কাজ করো। মরিয়ম ঘড়িতে ৮:৪২ দেখলেই ক্যামেরা তোলেন, ব্যান্ড ৮:৩০ দেখলেই বাজায়। আর কাজিনকে কিন্দি বললেন, মেসেজ পাঠাও ঠিক আছে, কিন্তু প্যান্ডেলের বন্ধুরা যেন মেসেজের সাথে লেখা সময়টা দেখে, ঘড়িতে সেই সময় না আসা পর্যন্ত না খোলে। শেষে কেক কাটার সময় কিন্দি সবাইকে একসাথে গুনতে বললেন, “তিন, দুই, এক”, আর যারা একটু আগে বা পরে গুনছিলেন তারা পাশের জনের সাথে তাল মিলিয়ে একটু ধীর বা দ্রুত করে নিলেন, থেমে যাননি।

মিলিয়ে নিই: রুশদের মাইকের ঘোষণা হলো লাইভ ভিডিও স্ট্রিম, আর পেছনের দিকে দেরিতে পৌঁছানো হলো প্রতিটা দর্শকের আলাদা লেটেন্সি। ছাপানো সময়সূচি হলো প্লেলিস্টের EXT-X-DATERANGE, আর মঞ্চের ঘড়ি হলো EXT-X-PROGRAM-DATE-TIME, যা প্রতিটা সেগমেন্টকে একটা ওয়াল-ক্লক সময়ের সাথে বাঁধে। মরিয়ম আর ব্যান্ডের ঘড়ি দেখে কাজ করা হলো নিজের দেখা মুহূর্তের ওয়াল-ক্লক সময় ধরে ইভেন্ট দেখানো, মানে প্লেয়ারের playingDate দিয়ে কুইজ বা স্কোর দেখানো। কাজিনের আগাম মেসেজ হলো স্পয়লার সমস্যা (চ্যাট আর নোটিফিকেশন ভিডিওর আগে আসে), আর মেসেজে সময় লিখে রাখা হলো চ্যাট মেসেজে স্ট্রিম-টাইম ট্যাগ। “তিন, দুই, এক”-এ তাল মিলিয়ে একটু ধীর-দ্রুত করা হলো ওয়াচ পার্টিতে playback rate nudging। বাস্তবে ক্রিকেট ম্যাচের লাইভে বল-বাই-বল স্কোর, কুইজ শো-তে প্রশ্ন, আর একসাথে দেখার ফিচার ঠিক এই নীতিতে চলে।

মূল সমস্যা: “এখন” সবার কাছে আলাদা

লাইভ স্ট্রিমে প্রতিটা দর্শক ভিন্ন লেটেন্সিতে দেখে। LL-HLS-এ একজন ৩ সেকেন্ড পেছনে, সাধারণ HLS-এ আরেকজন ২০ সেকেন্ড, আর বাফারিং হওয়া তৃতীয়জন হয়তো ৪০ সেকেন্ড (লো লেটেন্সি চ্যাপ্টারে লেটেন্সির উৎসগুলো দেখেছি)। এখন ধরুন প্রযোজক ঠিক ছক্কার মুহূর্তে একটা পোল দেখাতে চান, “এটা কি ম্যাচের সেরা শট?”

সরল সমাধান: সার্ভার ছক্কার মুহূর্তে WebSocket দিয়ে সবাইকে পোল পাঠায়। ফল: ৩ সেকেন্ডের দর্শক ছক্কা দেখার প্রায় সাথেই পোল পায়, ঠিক আছে। ২০ সেকেন্ডের দর্শক ছক্কা দেখার ১৭ সেকেন্ড আগে পোল পায়, যা একটা স্পয়লার। আর ৪০ সেকেন্ডের দর্শকের কাছে পোলটা মেয়াদ শেষ হয়ে যায় ছক্কা দেখার আগেই।

সমাধানের মূল ধারণা: ইভেন্টকে বাঁধুন ভিডিওর টাইমলাইনে, সার্ভারের ঘড়িতে নয়। প্রতিটা প্লেয়ার নিজের দেখা মুহূর্ত অনুযায়ী ইভেন্ট দেখাবে। এর জন্য দুটো জিনিস লাগে: ইভেন্টটা ভিডিওর কোন মুহূর্তের, সেটা প্রকাশ করার একটা উপায়, আর প্রতিটা প্লেয়ারের জানা যে সে এই মুহূর্তে ভিডিওর কোন সময়টা দেখাচ্ছে।

ভিডিওর সাথে মেটাডেটা পাঠানোর তিন উপায়

উপায়কোথায় থাকেসুবিধাঅসুবিধা
মিডিয়ার ভেতরে: ID3 (HLS), emsg (CMAF/DASH)সেগমেন্ট ফাইলের ভেতরেফ্রেম-স্তরের নির্ভুলতা; ভিডিওর সাথেই যায়এনকোডার বা packager-এ বসাতে হয়; পরে বদলানো যায় না
প্লেলিস্টে: EXT-X-DATERANGEmedia playlist-এpackager-ই বসায়; পড়া সহজ; CDN-এ ক্যাশ হয়প্লেলিস্ট রিফ্রেশের ছন্দে আসে
আলাদা চ্যানেলে: WebSocket বা SSEআপনার নিজের সার্ভারযেকোনো সময়, যেকোনো ডেটা; ভিডিও পাইপলাইন ছোঁয় নাপ্রতিটা বার্তায় স্ট্রিম-টাইম ট্যাগ লাগে; প্লেয়ারে মেলাতে হয়

Apple-এর HLS authoring spec (২০২৬ সালের অক্টোবরে দেখা) একটা স্পষ্ট পরামর্শ দেয়: মেটাডেটা মিডিয়ার ভেতরে (Timed Metadata) না রেখে প্লেলিস্টে (EXT-X-DATERANGE) রাখা উচিত। কারণ সহজ: প্লেলিস্ট টেক্সট, packager নিজেই লেখে, আর সব প্লেয়ার একইভাবে পড়ে। বাস্তবে বড় সিস্টেম প্রায়ই প্লেলিস্ট আর আলাদা চ্যানেল একসাথে চালায়: প্লেলিস্টে “কোন মুহূর্তে”, আলাদা চ্যানেলে “কী দেখাতে হবে” (পোলের প্রশ্ন, স্কোরের বিস্তারিত)।

PROGRAM-DATE-TIME: সব কিছুর নোঙর

EXT-X-PROGRAM-DATE-TIME (PDT) একটা সেগমেন্টের প্রথম ফ্রেমকে একটা পরম ওয়াল-ক্লক সময়ের সাথে বাঁধে। rfc8216bis খসড়া বলে মানটায় টাইম জোন আর অন্তত মিলিসেকেন্ড নির্ভুলতা থাকা উচিত, আর টাইম জোন না থাকলে ক্লায়েন্ট UTC ধরবে। এটাই সেই মঞ্চের ঘড়ি, যেটা দিয়ে সবার “এখন” মেলানো যায়।

তিনটা নিয়ম যা স্পেক থেকেই আসে, ২০২৬ সালের অক্টোবর অনুযায়ী:

  • Apple-এর spec: প্রতিটা লাইভ media playlist-এ PDT থাকতেই হবে, আর মানটা কনটেন্টের আসল সম্প্রচার সময়ের সাথে মেলা উচিত।
  • RFC 8216 আর খসড়া দুটোই: প্লেলিস্টে কোনো EXT-X-DATERANGE থাকলে অন্তত একটা PDT থাকতেই হবে। PDT ছাড়া DATERANGE-এর START-DATE-কে ভিডিওর কোনো মুহূর্তে মেলানো যায় না।
  • Apple-এর SharePlay অংশ বলে লাইভ কনটেন্টে PDT থেকে তৈরি টাইমলাইনই সিঙ্কের ভিত্তি, আর সেটা সব অংশগ্রহণকারীর কাছে এক হতে হবে।

PDT কতটা ঠিক, সেটা নির্ভর করে এনকোডারের ঘড়ির উপর। এনকোডার বা packager-এর সার্ভারে NTP (বা আরও নির্ভুল দরকার হলে PTP) না চললে PDT কয়েক সেকেন্ড ভুল হতে পারে, আর তখন সব সিঙ্ক সেই পরিমাণ ভুল। ডিসকন্টিনিউটির পরে নতুন PDT দিন (রিলায়েবিলিটি চ্যাপ্টারের প্লেলিস্ট দেখুন), নইলে প্লেয়ার পুরনো নোঙর থেকে হিসাব চালিয়ে যায় আর ভুল জমতে থাকে।

প্লেলিস্টে ইভেন্ট: EXT-X-DATERANGE

একটা কুইজের প্রশ্ন বা একটা উইকেটের মুহূর্তকে প্লেলিস্টে একটা date range হিসেবে বসানো যায়। RFC 8216 অনুযায়ী X- উপসর্গওয়ালা অ্যাট্রিবিউট ক্লায়েন্টের নিজস্ব ব্যবহারের জন্য সংরক্ষিত, তাই নিজের ডেটা সেখানে রাখা যায়। CLASS দিয়ে বলা যায় এটা কোন ধরনের ইভেন্ট।

# illustrative live media playlist with an interactive quiz window
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:448234240
#EXT-X-PROGRAM-DATE-TIME:2026-10-25T14:02:40.000Z
#EXT-X-DATERANGE:ID="quiz-17",CLASS="org.baytalhikma.quiz",START-DATE="2026-10-25T14:02:52.000Z",DURATION=20.0,X-QUESTION-ID="17"
#EXTINF:4.000,
seg_448234240.m4s
#EXTINF:4.000,
seg_448234241.m4s
#EXTINF:4.000,
seg_448234242.m4s
#EXTINF:4.000,
seg_448234243.m4s

প্লেয়ার এখান থেকে হিসাব করে: সেগমেন্ট 448234240 শুরু ১৪:০২:৪০-এ, তাই ১৪:০২:৫২ পড়ে চার নম্বর সেগমেন্টের শুরুতে। দর্শক ঠিক সেই ফ্রেমে পৌঁছালে কুইজ দেখায়, তার লেটেন্সি যতই হোক।

প্রশ্নের লেখা প্লেলিস্টে রাখবেন না। প্লেলিস্ট প্রতি কয়েক সেকেন্ডে লক্ষ লক্ষবার ডাউনলোড হয়; প্রতিটা বাইট গুণ হয়। প্লেলিস্টে শুধু আইডি আর সময় রাখুন, প্রশ্নের লেখা, ছবি আর উত্তরের বিকল্প আলাদা API বা আগাম WebSocket বার্তায় পাঠান। আর কুইজের সঠিক উত্তর কখনো আগাম ক্লায়েন্টে পাঠাবেন না; DevTools খুলে যে কেউ দেখে ফেলবে।

প্লেয়ারে মেটাডেটা ধরা

hls.js

hls.js-এর API ডকস (২০২৬ সালের অক্টোবরে দেখা) অনুযায়ী তিনটা কনফিগ ডিফল্টে true: enableID3MetadataCues, enableDateRangeMetadataCues, আর enableEmsgMetadataCues। মানে hls.js নিজেই ID3, EXT-X-DATERANGE আর CMAF emsg থেকে video এলিমেন্টের একটা metadata TextTrack-এ cue বানায়, আর প্লেব্যাক সেই সময়ে পৌঁছালে cue সক্রিয় হয়। এর সুবিধা হলো আপনাকে সময় হিসাব করতে হয় না; ব্রাউজারের নিজের cuechange ইভেন্টই বলে দেয় “এখন”।

// Show interactive overlays when their metadata cue becomes active during playback.
function watchMetadataCues(video: HTMLVideoElement, onEvent: (id: string, cue: TextTrackCue) => void) {
	const seen = new Set<string>();

	const attach = (track: TextTrack) => {
		if (track.kind !== 'metadata') return;
		track.mode = 'hidden'; // cues fire only when the track is not disabled
		track.addEventListener('cuechange', () => {
			for (const cue of Array.from(track.activeCues ?? [])) {
				if (seen.has(cue.id)) continue;
				seen.add(cue.id);
				onEvent(cue.id, cue);
			}
		});
	};

	Array.from(video.textTracks).forEach(attach);
	video.textTracks.addEventListener('addtrack', (e) => attach((e as TrackEvent).track as TextTrack));
}

cue-এর ভেতরে কী থাকে (DATERANGE-এর অ্যাট্রিবিউট, ID3 ফ্রেমের ডেটা) সেটা hls.js আর ব্রাউজার অনুযায়ী আলাদা আকারে আসে। তাই প্রথমবার console.log(cue) করে নিজের কনফিগে দেখে নিন। নিচু স্তরে ID3 বা KLV পার্স হওয়ার পর hls.js FRAG_PARSING_METADATA ইভেন্টও দেয়, যার samples অ্যারেতে প্রতিটা নমুনার pts/dts আর ডেটা থাকে; cue যথেষ্ট না হলে এটা ব্যবহার করুন। আর DATERANGE দিয়ে ইন্টারস্টিশিয়াল বিজ্ঞাপনের জন্য hls.js-এর আলাদা INTERSTITIAL_* ইভেন্ট আছে (প্লেয়ারে অ্যাড দেখুন)।

Safari-র নেটিভ HLS

Safari নিজেই HLS চালায়, hls.js লাগে না, আর টাইমড মেটাডেটাও video.textTracks-এ metadata ট্র্যাক হিসেবে দেয়। উপরের একই cuechange কোড সেখানেও চলে, তবে cue-এর ভেতরের আকার (WebKit-এর নিজস্ব cue টাইপ) hls.js-এর থেকে আলাদা হতে পারে। দুই পথেই একটা ছোট অ্যাডাপ্টার রাখুন যা cue থেকে আপনার ইভেন্ট আইডি বের করে। কোন ভার্সনে কী আসে, সেটা নিজের টার্গেট Safari-তে পরীক্ষা করুন।

ওয়াল-ক্লক দিয়ে মেলানো

আলাদা চ্যানেলে আসা ইভেন্টের জন্য (WebSocket থেকে আসা পোল, স্কোর) cue নেই, তাই প্লেয়ারকে জানতে হবে সে এখন কোন ওয়াল-ক্লক মুহূর্ত দেখাচ্ছে। hls.js-এ সেটা hls.playingDate: ডকস অনুযায়ী এটা সক্রিয় লেভেলের PDT থেকে media.currentTime-এর সাপেক্ষে হিসাব করা তারিখ-সময়। Safari-তে video.getStartDate() প্লেলিস্টের PDT থেকে টাইমলাইনের শুরুর তারিখ দেয়, যার সাথে currentTime যোগ করলে একই জিনিস পাওয়া যায়।

// Release out-of-band events (polls, scores) only when this viewer's picture reaches them.
type StreamEvent = { id: string; streamTime: number }; // ms since epoch, from PDT
const pending: StreamEvent[] = [];

function currentStreamTime(video: HTMLVideoElement, hls?: { playingDate: Date | null }): number | null {
	if (hls?.playingDate) return hls.playingDate.getTime();
	const start = (video as HTMLVideoElement & { getStartDate?: () => Date }).getStartDate?.();
	if (start && !Number.isNaN(start.getTime())) return start.getTime() + video.currentTime * 1000;
	return null;
}

function tick(video: HTMLVideoElement, hls: { playingDate: Date | null } | undefined, show: (e: StreamEvent) => void) {
	const now = currentStreamTime(video, hls);
	if (now !== null) {
		while (pending.length && pending[0].streamTime <= now) show(pending.shift()!);
	}
	requestAnimationFrame(() => tick(video, hls, show));
}

// on WebSocket message: insert sorted by streamTime, then let tick() release it

setTimeout দিয়ে নয়, প্লেব্যাকের সময় দেখে ছাড়া হয় বলে দর্শক পজ করলে ইভেন্টও অপেক্ষা করে, আর বাফারিং হলে ইভেন্ট আগে চলে আসে না।

চ্যাট সিঙ্ক: দর্শকের দেখা মুহূর্তে মেসেজ

চ্যাট রিয়েল-টাইম, ভিডিও দেরিতে। ৩ সেকেন্ডের দর্শক উইকেট দেখে লেখে “আউট!”, আর ২০ সেকেন্ডের দর্শক সেটা পড়ে উইকেট দেখার ১৭ সেকেন্ড আগে। দুটো পথ:

১. লেটেন্সি কমানো। সবাই ২-৩ সেকেন্ডের মধ্যে থাকলে সমস্যা প্রায় অদৃশ্য। LL-HLS বা WebRTC। কিন্তু সবাইকে কম লেটেন্সিতে রাখা যায় না (দুর্বল নেটওয়ার্কে প্লেয়ার নিজেই পিছিয়ে যায়)।

২. প্রতিটা মেসেজে স্ট্রিম-টাইম ট্যাগ। এখানে একটা সূক্ষ্ম কিন্তু জরুরি সিদ্ধান্ত: ট্যাগ হবে লেখকের দেখা মুহূর্ত, সার্ভারে পৌঁছানোর সময় নয়। ৩ সেকেন্ডের দর্শক “আউট!” লিখেছেন উইকেটের ফ্রেম দেখার পর, তাই মেসেজটার আসল অর্থ “PDT ১৪:০৩:১২-এর ফ্রেম সম্পর্কে”। প্রতিটা পাঠক মেসেজটা দেখাবে যখন তার নিজের প্লেয়ার ১৪:০৩:১২-এ পৌঁছায়। সার্ভারের সময় দিয়ে ট্যাগ করলে লেখকের নিজের লেটেন্সি মেসেজের সাথে যোগ হয়ে যেত।

// Sender: tag the message with the picture the author was looking at.
function sendChat(ws: WebSocket, text: string, video: HTMLVideoElement, hls?: { playingDate: Date | null }) {
	const streamTime = currentStreamTime(video, hls);
	ws.send(JSON.stringify({ type: 'chat', text, streamTime })); // null when there is no PDT: show immediately
}

// Receiver: own messages render at once; everyone else's wait for this viewer's picture.

তিনটা ব্যবহারিক নিয়ম: নিজের মেসেজ নিজের স্ক্রিনে সাথে সাথে দেখান (নইলে মনে হয় পাঠানো হয়নি); একটা সর্বোচ্চ বিলম্ব রাখুন (ধরুন ৬০ সেকেন্ড), তার বেশি পিছিয়ে থাকা দর্শকের জন্য মেসেজ আর আটকে না রেখে দেখিয়ে দিন; আর মডারেশন সার্ভারে হোক মেসেজ আসার সাথে সাথে, দেখানোর সময়ে নয়।

স্পয়লার সমস্যা: ভিডিওর বাইরের সব কিছু

চ্যাট ঠিক করলেও স্পয়লার আসে অন্য পথে: আপনার নিজের অ্যাপের পুশ নোটিফিকেশন (“বাংলাদেশ জিতেছে!”), স্কোরবোর্ড উইজেট যা আলাদা স্কোর API থেকে আসে, আর সোশ্যাল মিডিয়া। শেষটা আপনার নিয়ন্ত্রণে নেই, কিন্তু প্রথম দুটো আছে।

  • অ্যাপের ভেতরের স্কোর উইজেট ভিডিওর সাথে মেলান: স্কোর আপডেটে স্ট্রিম-টাইম ট্যাগ দিন, উপরের tick() দিয়ে ছাড়ুন। স্কোর API ভিডিওর চেয়ে দ্রুত, তাই এটা না করলে উইজেট ক্রমাগত স্পয়লার দেয়।
  • পুশ নোটিফিকেশন যারা এই মুহূর্তে লাইভ দেখছে তাদের কাছে না পাঠানো, বা পাঠানোর আগে তাদের লেটেন্সির সমান দেরি করা। যারা দেখছে না তাদের কাছে পাঠানোই নোটিফিকেশনের উদ্দেশ্য।
  • দর্শককে সৎভাবে জানান তিনি কত পেছনে আছেন (“লাইভ থেকে ২০ সেকেন্ড পেছনে”), আর একটা “লাইভে যান” বোতাম দিন। লুকিয়ে রাখার চেয়ে এটা বেশি সম্মানজনক।

ওয়াচ পার্টি: সবাইকে একই ফ্রেমে রাখা

পরিবারের পাঁচজন পাঁচ শহর থেকে একসাথে ম্যাচ দেখতে চান, আর ভিডিও কলে একে অপরের প্রতিক্রিয়া দেখতে চান। একজন ৫ সেকেন্ড আগে থাকলে তার চিৎকারই সবার স্পয়লার।

১. একটা সাধারণ লক্ষ্য বেছে নিন। গ্রুপের সবাই একই PDT মুহূর্তে থাকবে, যা নির্ধারিত হয় “এখনকার ওয়াল-ক্লক বিয়োগ একটা টার্গেট লেটেন্সি” দিয়ে। টার্গেট হওয়া উচিত গ্রুপের সবচেয়ে ধীর সদস্য যা টানতে পারে, কারণ দ্রুতকে পিছিয়ে আনা সহজ, ধীরকে এগিয়ে নেওয়া সম্ভব নয় যদি তার নেটওয়ার্ক সেটা না দেয়।

২. সিক নয়, রেট বদলান। ছোট ফারাক (এক-দুই সেকেন্ড) playbackRate সামান্য বদলে মেটান, যেমন ০.৯৫ বা ১.০৫। দর্শক এটা প্রায় টের পান না। সিক করলে বাফার ভেঙে যায়, রিবাফার হয়, আর ফারাক বাড়ে। শুধু বড় ফারাকে (ধরুন তিন সেকেন্ডের বেশি) সিক করুন।

৩. একটা ডেডব্যান্ড রাখুন। আধা সেকেন্ডের কম ফারাকে কিছু করবেন না। নইলে প্লেয়ার ক্রমাগত দ্রুত-ধীর করতে থাকে, যা অডিওতে শোনা যায়।

// Nudge this player toward the group's target picture time without seeking for small drift.
function syncToGroup(video: HTMLVideoElement, myStreamTime: number, targetStreamTime: number) {
	const driftMs = myStreamTime - targetStreamTime; // positive: I am ahead of the group
	const abs = Math.abs(driftMs);

	if (abs < 500) {
		video.playbackRate = 1; // deadband: close enough
	} else if (abs < 3000) {
		video.playbackRate = driftMs > 0 ? 0.95 : 1.05;
	} else {
		video.currentTime -= driftMs / 1000; // large drift: one seek, then nudge
		video.playbackRate = 1;
	}
}

hls.js-এ লাইভ এজের দূরত্ব ধরে রাখার নিজস্ব ব্যবস্থাও আছে: ডকস অনুযায়ী maxLiveSyncPlaybackRate (ডিফল্ট ১, যা রেট বদলানো বন্ধ রাখে; সর্বোচ্চ ২) বাড়ালে hls.js নিজেই একটু দ্রুত চালিয়ে টার্গেটে ফেরে, আর hls.targetLatency সেট করা যায়। গ্রুপের সবাই একই targetLatency নিলে আর এনকোডারের PDT ঠিক থাকলে অনেক ক্ষেত্রে এটুকুই যথেষ্ট; উপরের নিজস্ব কন্ট্রোলার লাগে যখন গ্রুপের সদস্যরা আলাদা প্লেয়ার বা আলাদা ডিভাইসে।

WebSocket ফ্যান-আউট: ভিডিওর লেটেন্সি এখানে বন্ধু

লাখো দর্শকের কাছে একসাথে একটা কুইজ পাঠাতে WebSocket ফ্যান-আউট লাগে, যার স্কেলিং WebSockets ট্র্যাকের pub/sub স্কেল চ্যাপ্টারে আছে। লাইভে একটা সুবিধা আছে যা অন্য রিয়েল-টাইম অ্যাপে নেই: ভিডিওর লেটেন্সি আপনাকে ফ্যান-আউটের জন্য সময় দেয়।

প্রযোজক ১৪:০২:৫২-এ কুইজ শুরু করলেন। সবচেয়ে কম লেটেন্সির দর্শক সেই ফ্রেম দেখবেন ধরুন ১৪:০২:৫৫-এ। মানে প্রশ্নটা সব দর্শকের কাছে পৌঁছানোর জন্য আপনার হাতে অন্তত তিন সেকেন্ড, বেশিরভাগের জন্য আরও বেশি। তাই কুইজের বার্তা সাথে সাথে পাঠান, স্ট্রিম-টাইম ট্যাগ দিয়ে, আর প্লেয়ার সেটা জমিয়ে রেখে ঠিক মুহূর্তে দেখাক। ফ্যান-আউটের দেরি তখন দর্শকের চোখে অদৃশ্য।

উল্টো দিকে সতর্কতা: সবাই একই সেকেন্ডে উত্তর দেয়। দর্শকরা আলাদা লেটেন্সিতে প্রশ্ন দেখেন বলে উত্তরও কিছুটা ছড়িয়ে আসে, কিন্তু LL স্ট্রিমে সেই ছড়ানো খুব কম। উত্তর গ্রহণকারী সার্ভিস প্রত্যাশিত পিকের জন্য স্কেল করুন, উত্তর একটা কিউতে নিন, আর সময়সীমা বিচার করুন দর্শকের দেখা মুহূর্ত থেকে, সার্ভারের ঘড়ি থেকে নয়, নইলে বেশি লেটেন্সির দর্শক অন্যায্যভাবে কম সময় পান।

An interactive event, released by each viewer's own picture time

ডিবাগিং: এই চ্যাপ্টারের সমস্যাগুলো

লক্ষণনিশ্চিত করার উপায়সমাধান
কুইজ বা স্কোর ভিডিওর আগে দেখায়ইভেন্ট কোন ঘড়ি দিয়ে ছাড়া হচ্ছে দেখুন: সার্ভারের সময়, setTimeout, নাকি প্লেয়ারের playingDateপ্লেব্যাকের স্ট্রিম-টাইম দিয়ে ছাড়ুন; WebSocket বার্তায় streamTime
metadata cue কখনো ফায়ার হয় নাvideo.textTracks-এ kind = metadata ট্র্যাক আছে কিনা, আর তার modeট্র্যাকের mode = hidden; hls.js-এ enable*MetadataCues বন্ধ করা হয়নি তো দেখুন
DATERANGE ইভেন্ট আসে না, প্লেলিস্টে ট্যাগ আছেপ্লেলিস্টে EXT-X-PROGRAM-DATE-TIME আছে কিনাDATERANGE থাকলে অন্তত একটা PDT বাধ্যতামূলক
ইভেন্ট ঘণ্টাখানেক বা কয়েক ঘণ্টা সরে আছেPDT আর START-DATE-এ টাইম জোন বা Z আছে কিনাসবসময় UTC-তে, Z সহ, মিলিসেকেন্ড সহ
ইভেন্ট কয়েক সেকেন্ড সরে আছে, সব দর্শকের একই পরিমাণেএনকোডার বা packager সার্ভারে timedatectl বা chronyc tracking দিয়ে ঘড়ির অফসেট দেখুনNTP চালু ও সিঙ্ক; সঠিকতা জরুরি হলে PTP
এনকোডার ফেইলওভারের পর থেকে সব ইভেন্ট সরে গেছেডিসকন্টিনিউটির পরে নতুন PDT আছে কিনাপ্রতিটা ডিসকন্টিনিউটির পরে নতুন PDT
Safari-তে চলে, hls.js-এ চলে না, বা উল্টোদুই জায়গায় console.log(cue) করে আকার তুলনা করুনcue থেকে ইভেন্ট আইডি বের করার জন্য দুই পথের আলাদা অ্যাডাপ্টার
চ্যাটে “আউট!” উইকেটের আগে আসেমেসেজের ট্যাগ কোন সময়ের: লেখকের playingDate নাকি সার্ভারেরলেখকের দেখা মুহূর্ত দিয়ে ট্যাগ; পাঠকের প্লেয়ারে মিলিয়ে ছাড়ুন
ওয়াচ পার্টিতে অডিও কাঁপে, প্লেয়ার দ্রুত-ধীর করতে থাকেপ্রতি সেকেন্ডে playbackRate-এর লগ দেখুনডেডব্যান্ড যোগ করুন; রেটের ধাপ ছোট রাখুন
কুইজের মুহূর্তে উত্তর সার্ভিস পড়ে যায়উত্তরের রিকোয়েস্ট রেটের কাঁটা দেখুনআগে থেকে স্কেল; উত্তর কিউতে; দর্শকের দেখা মুহূর্ত থেকে সময়সীমা

মূল শিক্ষা

  1. লাইভে “এখন” প্রতিটা দর্শকের কাছে আলাদা। ইভেন্টকে বাঁধুন ভিডিওর টাইমলাইনে, সার্ভারের ঘড়িতে নয়।
  2. PROGRAM-DATE-TIME সব সিঙ্কের নোঙর। প্রতিটা লাইভ প্লেলিস্টে, UTC-তে, মিলিসেকেন্ড সহ, ডিসকন্টিনিউটির পরে আবার, আর এনকোডারের ঘড়ি NTP-তে।
  3. মেটাডেটা প্লেলিস্টে রাখুন (EXT-X-DATERANGE), বড় কনটেন্ট আলাদা চ্যানেলে; প্লেলিস্টে শুধু আইডি আর সময়।
  4. প্লেয়ারের metadata cue বা playingDate দিয়ে ছাড়ুন, setTimeout দিয়ে নয়, যাতে পজ আর বাফারিং সঠিকভাবে সামলানো হয়।
  5. চ্যাট মেসেজে লেখকের দেখা মুহূর্ত ট্যাগ করুন, আর প্রতিটা পাঠক নিজের প্লেয়ারে মিলিয়ে দেখাক।
  6. স্পয়লারের উৎস প্রায়ই আপনার নিজের অ্যাপ। স্কোর উইজেট আর নোটিফিকেশনকে ভিডিওর সাথে মেলান, আর দর্শককে সৎভাবে জানান তিনি কত পেছনে।
  7. ওয়াচ পার্টিতে সিক নয়, রেট, ডেডব্যান্ড সহ; শুধু বড় ফারাকে সিক।
  8. ভিডিওর লেটেন্সি ফ্যান-আউটের জন্য সময় দেয়। ইভেন্ট আগাম পাঠান, ঠিক মুহূর্তে দেখান।