Skip to content

DVR, টাইমশিফট ও live-to-VOD

লাইভে পেছনে টানা, শুরু থেকে দেখা, সম্প্রচার চলতে চলতেই রেকর্ড করা, PROGRAM-DATE-TIME দিয়ে হাইলাইট কাটা, আর শেষ হলে রেকর্ডিংকে VOD-তে তুলে দেওয়া — সাথে স্টোরেজ আর প্লেলিস্টের আকারের হিসাব।

গল্পে বুঝি

ঢাকা জজ কোর্টের তিন নম্বর এজলাসে শুনানি চলছে। বিচারকের ডান পাশে ছোট একটা টেবিলে বসে আছেন স্টেনোগ্রাফার রুশদ। সাক্ষী যা বলছেন, আইনজীবী যা প্রশ্ন করছেন, রুশদ সব টুকে যাচ্ছেন, প্রতিটা পাতার কোণে ঘড়ি দেখে সময় লিখে রাখছেন: সকাল ১০টা ৪০, ১০টা ৪৫। শুনানি থামে না, রুশদও থামেন না।

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

দিন শেষে রুশদ খাতার শেষ পাতায় লিখলেন “আজকের মতো মুলতবি”, সই করলেন। খাতাটা গেল নকলখানায়, যেখানে পরিষ্কার করে টাইপ হয়ে নথিতে উঠবে, আর পরের মাসে যে কেউ সার্টিফায়েড কপি তুলতে পারবে। একজন সাংবাদিক শুধু সাক্ষীর জেরার অংশটুকুর কপি চাইলেন, ১১টা ১০ থেকে ১১টা ২৫। পুরো দিনের খাতা নয়, শুধু ওই পাতাগুলো। আর পুরনো খাতা কতদিন রাখা হবে, কবে নষ্ট করা হবে, তার নিয়ম আদালতের রেকর্ডরুমে লেখা।

মিলিয়ে নিই: চলমান শুনানি হলো লাইভ স্ট্রিম, আর রুশদের খাতা হলো রেকর্ড হতে থাকা সেগমেন্ট। প্রতিটা পাতার কোণের সময় হলো EXT-X-PROGRAM-DATE-TIME। শুনানি চলার মাঝে সকালের অংশ পড়ে শোনানো হলো DVR বা টাইমশিফট, আর ফাতিমার শুরু থেকে পড়া হলো start-over। “আজকের মতো মুলতবি” লেখা হলো EXT-X-ENDLIST দিয়ে প্লেলিস্ট বন্ধ করা, আর নকলখানায় টাইপ হয়ে নথিতে ওঠা হলো live-to-VOD: VOD পাইপলাইনে রি-প্যাকেজ বা রি-এনকোড। সাংবাদিকের চাওয়া ১১টা ১০ থেকে ১১টা ২৫ হলো PDT দিয়ে ক্লিপ কাটা, আর রেকর্ডরুমের নিয়ম হলো রিটেনশন পলিসি। বাস্তবে খেলা, টক শো আর ক্লাসের প্রতিটা লাইভ প্ল্যাটফর্ম এই ব্যবস্থাগুলোই দেয়।

চারটা আলাদা জিনিস, প্রায়ই এক নামে

নামদর্শক কী করতে পারেকোথা থেকে আসে
DVR / টাইমশিফটচলমান লাইভে কয়েক মিনিট বা ঘণ্টা পেছনে টানা, আবার লাইভে ফেরালম্বা জানালার লাইভ প্লেলিস্ট
Start-overচলমান প্রোগ্রাম শুরু থেকে দেখাপ্রোগ্রামের শুরু থেকে প্লেলিস্ট
Live-to-VODসম্প্রচার শেষে পুরোটা VOD হিসেবে দেখারেকর্ড করা সেগমেন্ট, বন্ধ প্লেলিস্ট
Catch-upগত কয়েকদিনের যেকোনো প্রোগ্রাম দেখা (টিভি চ্যানেলের মতো)রেকর্ডিং থেকে প্রোগ্রামের সময় ধরে কাটা

চারটার ভিত্তি একই: সেগমেন্টগুলো মুছে না ফেলে রেখে দেওয়া, আর প্রতিটার বাস্তব সময় জানা।

DVR জানালা: তিন রকম প্লেলিস্ট

আগের চ্যাপ্টারের লাইভ প্লেলিস্টে শুধু শেষ কয়েকটা সেগমেন্ট থাকত, তাই দর্শক বড়জোর ৩০-৬০ সেকেন্ড পেছনে যেতে পারত। DVR দিতে হলে প্লেলিস্টে বেশি সেগমেন্ট রাখতে হয়। তিনটা প্রচলিত আকার:

১. লম্বা স্লাইডিং জানালা। একই স্লাইডিং প্লেলিস্ট, শুধু অনেক বড়। দুই ঘণ্টার DVR চাইলে, ২ সেকেন্ডের সেগমেন্টে:

-hls_time 2 -hls_list_size 3600 -hls_flags delete_segments+program_date_time+independent_segments

জানালা সবসময় শেষ দুই ঘণ্টা; তার আগেরটা মুছে যায়। চব্বিশ ঘণ্টা চলা চ্যানেলের জন্য এটাই স্বাভাবিক।

২. EVENT প্লেলিস্ট। শুরু থেকে সব সেগমেন্ট থাকে, নতুনটা শুধু যোগ হয়, কিছু বাদ পড়ে না। একটা নির্দিষ্ট দৈর্ঘ্যের অনুষ্ঠানের (ম্যাচ, কনসার্ট) জন্য উপযুক্ত, কারণ যেকোনো দর্শক শুরু থেকে দেখতে পারে।

-hls_time 2 -hls_playlist_type event -hls_flags program_date_time+independent_segments

-hls_playlist_type event দিলে ffmpeg নিজেই hls_list_size ০ (সীমাহীন) ধরে নেয়, আর কোনো সেগমেন্ট মোছে না। ১২ সেকেন্ডের একটা পরীক্ষামূলক চালানোর আসল আউটপুট:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:2.000000,
#EXT-X-PROGRAM-DATE-TIME:2026-10-25T10:48:35.246+0600
seg_00000.ts
#EXTINF:2.000000,
#EXT-X-PROGRAM-DATE-TIME:2026-10-25T10:48:37.246+0600
seg_00001.ts
...
#EXTINF:2.000000,
#EXT-X-PROGRAM-DATE-TIME:2026-10-25T10:48:45.246+0600
seg_00005.ts
#EXT-X-ENDLIST

MEDIA-SEQUENCE চিরকাল ০ থাকে, কারণ প্রথম সেগমেন্টটা কখনো বাদ পড়ে না। আর ইনপুট শেষ হলে ffmpeg ENDLIST লিখেছে; এখানে সেটাই চাই, কারণ এর পর প্লেলিস্টটা কার্যত একটা VOD প্লেলিস্ট। (লাইভ চলার মাঝে ffmpeg ক্র্যাশ করলে একই ENDLIST সমস্যা করে; আগের চ্যাপ্টারের omit_endlist আলোচনা দেখুন।)

৩. দুটো আলাদা প্লেলিস্ট। দর্শকের জন্য একটা ছোট স্লাইডিং প্লেলিস্ট, আর রেকর্ডিংয়ের জন্য একটা EVENT প্লেলিস্ট যেটা শুধু DVR বা start-over চাইলে দেওয়া হয়। বেশিরভাগ দর্শক লাইভেই থাকে, তাদের বড় প্লেলিস্ট বারবার ডাউনলোড করতে হয় না। এর কারণ পরের অংশে।

লম্বা প্লেলিস্টের দাম

একটা প্লেলিস্ট এন্ট্রি দেখতে ছোট, কিন্তু প্লেয়ার লাইভে প্রতি সেগমেন্ট দৈর্ঘ্যে একবার পুরো প্লেলিস্ট আবার নামায়। আসল আউটপুটে একটা সেগমেন্টের তিন লাইন (EXTINF, PROGRAM-DATE-TIME, ফাইলের নাম) মিলে প্রায় ৮৫ বাইট। তিন ঘণ্টার একটা ম্যাচ, ২ সেকেন্ডের সেগমেন্ট, EVENT প্লেলিস্ট:

3 h x 3600 s / 2 s  = 5,400 segments
5,400 x ~85 bytes   = ~459 KB per playlist (measured on a generated 5,400-entry playlist: 459,100 bytes)
gzip                = 29,932 bytes
brotli              = 11,698 bytes

শেষের দিকে প্রতিটা দর্শক প্রতি ২ সেকেন্ডে প্রতিটা রাংয়ের জন্য প্রায় অর্ধ মেগাবাইট টানছে, শুধু জানতে যে নতুন একটা সেগমেন্ট এসেছে। এক লাখ দর্শকে সংখ্যাটা ভয়ংকর, আর ধীর মোবাইলে প্লেলিস্ট নামাতে নামাতেই পরেরটার সময় হয়ে যায়। তিনটা প্রতিকার:

  • CDN-এ প্লেলিস্ট কমপ্রেশন চালু রাখুন। উপরের মাপে gzip প্রায় ১৫ ভাগের এক ভাগে আর brotli প্রায় ৪০ ভাগের এক ভাগে নামিয়েছে, কারণ প্লেলিস্টের লাইনগুলো প্রায় একই রকম। এটা সবচেয়ে সস্তা জয়।
  • বেশিরভাগ দর্শককে ছোট প্লেলিস্ট দিন (উপরের তিন নম্বর আকার)।
  • প্লেলিস্ট ডেল্টা আপডেট। HLS স্পেকের নতুন সংস্করণে সার্ভার EXT-X-SERVER-CONTROL-এ CAN-SKIP-UNTIL ঘোষণা করতে পারে, আর প্লেয়ার _HLS_skip=YES দিয়ে চাইলে পুরনো অংশের বদলে একটা EXT-X-SKIP লাইন পায়। এটা কাজ করতে হলে আপনার origin সফটওয়্যার আর প্লেয়ার দুটোকেই এটা সমর্থন করতে হবে; ব্যবহারের আগে দুটোর ডকুমেন্টেশনে মিলিয়ে নিন।

স্টোরেজের হিসাব

DVR মানে সেগমেন্ট মোছা হচ্ছে না, তাই স্টোরেজ বাড়ে প্রতিটা রাংয়ের বিটরেটের যোগফলের হারে। আগের চ্যাপ্টারের ladder দিয়ে:

video: 5.0 + 2.8 + 1.2 Mbps = 9.0 Mbps
audio: 3 x 128 kbps         = 0.384 Mbps
total                       = 9.384 Mbps
per hour: 9.384 x 3600 / 8  = ~4,223 MB = ~4.2 GB per stream-hour

দুই ঘণ্টার DVR জানালা মানে প্রতিটা চলমান স্ট্রিমে origin-এ প্রায় সাড়ে আট GB। দশটা চ্যানেল ২৪ ঘণ্টা রেকর্ড করলে দিনে প্রায় এক TB। এই হিসাবটা চালু করার আগেই করে রাখুন; মাঝপথে ডিস্ক ভরে গেলে ট্রান্সকোডার নতুন সেগমেন্ট লিখতে পারে না, আর লাইভ নিজেই থেমে যায়।

লাইভ origin-এর ডিস্ক আর রেকর্ডিংয়ের স্টোরেজ আলাদা রাখুন। লাইভ origin-এ শুধু DVR জানালাটুকু থাকুক; রেকর্ডিং যাক object storage-এ। দুটো একই ডিস্কে থাকলে আর্কাইভ বাড়তে বাড়তে একদিন চলমান লাইভের জায়গা খেয়ে ফেলবে।

প্লেয়ারে পেছনে টানা আর লাইভে ফেরা

প্লেয়ারের কাছে DVR আলাদা কোনো ফিচার নয়। প্লেলিস্টে যত সেগমেন্ট আছে ততটুকুই video.seekable রেঞ্জ, আর দর্শক ওই রেঞ্জের মধ্যে যেখানে খুশি যেতে পারে। কাজটা আসলে UI-র:

const video = document.querySelector('video')!;

// The seekable window grows (EVENT) or slides (long window) as the playlist updates
function dvrState() {
	const r = video.seekable;
	if (r.length === 0) return null;
	const start = r.start(0);
	const liveEdge = r.end(r.length - 1);
	return { start, liveEdge, behind: liveEdge - video.currentTime };
}

// "LIVE" button: jump back to a few seconds behind the edge, not exactly to it
function goLive(holdBack = 6) {
	const s = dvrState();
	if (s) video.currentTime = Math.max(s.start, s.liveEdge - holdBack);
}

তিনটা UI নিয়ম যেটা দর্শক আশা করে:

  • লাইভ থেকে কত পেছনে আছে দেখান (“লাইভ থেকে ৪ মিনিট পেছনে”), আর একটা স্পষ্ট “লাইভে ফিরুন” বোতাম রাখুন।
  • লাইভে ফেরা মানে একেবারে শেষে নয়। একদম শেষ মুহূর্তে লাফ দিলে প্রথম সেকেন্ডেই বাফার শেষ হবে। শেষ থেকে HOLD-BACK-এর সমান পেছনে যান (উপরের কোডে holdBack)।
  • পেছনে থাকা দর্শককে নিজে থেকে লাইভে টেনে আনবেন না। আগের চ্যাপ্টারের “ধরে ফেলা” (playback rate বাড়ানো) শুধু লাইভে থাকা দর্শকের জন্য। যে ইচ্ছে করে দশ মিনিট পেছনে গেছে, তাকে দ্রুত চালিয়ে সামনে আনা মানে তার পছন্দ উল্টে দেওয়া। hls.js-এর মতো লাইব্রেরি সাধারণত দর্শক নিজে সিক করলে এটা বুঝে নেয়; আপনার প্লেয়ারে এই আচরণটা পরীক্ষা করে দেখুন।

Start-over: শুরু থেকে খোলা

দর্শক যখন প্রোগ্রামের মাঝখানে আসে, কিছু প্ল্যাটফর্ম জিজ্ঞেস করে “শুরু থেকে দেখবেন নাকি লাইভে?” শুরু থেকে খোলার দুটো উপায়: প্লেয়ার লোড হওয়ার পর video.currentTime = video.seekable.start(0) বসানো, বা প্লেলিস্টেই প্লেয়ারকে বলে দেওয়া কোথা থেকে শুরু করতে হবে:

#EXT-X-START:TIME-OFFSET=0,PRECISE=YES

TIME-OFFSET ধনাত্মক হলে প্লেলিস্টের শুরু থেকে গোনা হয়, ঋণাত্মক হলে শেষ থেকে। এটা HLS স্পেকের (RFC 8216) একটা পুরনো ট্যাগ, কিন্তু সব প্লেয়ার একইভাবে মানে না; আপনার টার্গেট প্লেয়ারে পরীক্ষা করে দেখুন। start-over-এর জন্য সাধারণত আলাদা একটা প্লেলিস্ট URL দেওয়া হয় যেটা প্রোগ্রামের প্রথম সেগমেন্ট থেকে শুরু, যাতে লাইভের দর্শকদের প্লেলিস্ট ছোট থাকে।

লাইভ চলতে চলতেই রেকর্ড করা

সেগমেন্টগুলো এমনিতেই ফাইল হিসেবে লেখা হচ্ছে, তাই রেকর্ডিং মানে মূলত “মুছে ফেলার আগে নিরাপদ জায়গায় কপি করা”। সবচেয়ে সরল নির্ভরযোগ্য পথ:

লাইভ চলার মাঝেই রেকর্ডিং

ট্রান্সকোডার একই সেগমেন্টের জন্য দুটো প্লেলিস্ট লেখে, বা একটা EVENT প্লেলিস্ট লেখে যেখান থেকে লাইভের ছোট প্লেলিস্ট আলাদাভাবে বানানো হয়। আর্কাইভ ফোল্ডারটা একটা আলাদা প্রসেস ক্রমাগত object storage-এ তুলে দেয়। S3-কম্প্যাটিবল যেকোনো স্টোরেজে (Garage, S3) rclone দিয়ে:

# Upload new archive files every few seconds; skip ffmpeg's temp files (temp_file flag writes *.tmp first)
while true; do
  rclone copy --exclude "*.tmp" /srv/archive/match-2026-10-25/ \
    store:recordings/live-archive/match-2026-10-25/
  sleep 5
done

rclone copy আগে তোলা ফাইল আবার তোলে না, তাই লুপটা সস্তা। store হলো rclone config-এ বানানো S3 রিমোট। MinIO-র mc mirror --watch একসময় এই কাজের জনপ্রিয় টুল ছিল, কিন্তু MinIO-র কমিউনিটি এডিশন ২০২৫ সালের শেষ থেকে মেইনটেন্যান্স মোডে (২০২৬ সালের অক্টোবর অনুযায়ী); নতুন সেটআপে সেদিকে না যাওয়াই ভালো।

এখানে দুটো জিনিস মনে রাখুন:

  • ফাইল সম্পূর্ণ লেখা হওয়ার পর তুলুন। অর্ধেক লেখা সেগমেন্ট তুলে ফেললে রেকর্ডিংয়ে ভাঙা অংশ থাকবে। ffmpeg-এর temp_file ফ্ল্যাগ ফাইলটা আগে অস্থায়ী নামে লেখে, তারপর নাম বদলায়; অস্থায়ী নামগুলো বাদ দিয়ে তুললে এই সমস্যা থাকে না।
  • আপলোড পিছিয়ে পড়লে জানুন। object storage ধীর হলে আপলোডের সারি লম্বা হয়, আর লাইভ ডিস্ক থেকে সেগমেন্ট মুছে যাওয়ার আগে তোলা না হলে রেকর্ডিংয়ে ফাঁক থাকবে। আর্কাইভ ফোল্ডারে মোছা বন্ধ রাখার এটাই কারণ; সফল আপলোডের পরে অন্য একটা প্রসেস পুরনো ফাইল পরিষ্কার করবে।

প্লেলিস্ট বন্ধ করা আর VOD-তে তুলে দেওয়া

সম্প্রচার শেষে তিনটা ধাপ:

১. প্লেলিস্ট বন্ধ করুন। EVENT প্লেলিস্টের শেষে #EXT-X-ENDLIST যোগ হলে প্লেয়ার বোঝে আর নতুন কিছু আসবে না, আর পুরো রেঞ্জে সিক করতে দেয়। এই মুহূর্ত থেকে রেকর্ডিং দর্শকের কাছে কার্যত VOD। চাইলে PLAYLIST-TYPE-কে VOD করে দিতে পারেন।

২. সাথে সাথে দেখানোর জন্য রিমাক্স। ডাউনলোড বা অন্য প্ল্যাটফর্মে দেওয়ার জন্য একটা MP4 লাগলে রি-এনকোড না করে কন্টেইনার বদলালেই চলে:

ffmpeg -i index.m3u8 -c copy -bsf:a aac_adtstoasc -movflags +faststart full.mp4

১২ সেকেন্ডের পরীক্ষামূলক রেকর্ডিং থেকে এটা আসলে চালিয়ে পাওয়া MP4-এর দৈর্ঘ্য ১২.০৩ সেকেন্ড, কয়েক সেকেন্ডে। aac_adtstoasc দরকার কারণ TS-এর ভেতরের AAC অডিও আর MP4-এর AAC অডিও আলাদা মোড়কে থাকে।

৩. VOD পাইপলাইনে পাঠান। লাইভের এনকোড করা হয়েছিল দ্রুত preset-এ, তাড়াহুড়োয়, CBR-এ। রেকর্ডিং যদি দীর্ঘদিন দেখা হবে, VOD পাইপলাইনে ধীর preset আর per-title ladder দিয়ে আবার এনকোড করলে একই মানে অনেক কম বিটরেট লাগে, মানে দীর্ঘমেয়াদে কম স্টোরেজ আর কম ইগ্রেস। রেকর্ডিং object storage-এ পৌঁছানোর পর VOD প্ল্যাটফর্মকে একটা এনকোডিং জব পাঠানোই সবচেয়ে পরিষ্কার সীমারেখা: লাইভ সিস্টেমের কাজ শেষ, এখন থেকে এটা আর দশটা আপলোড করা ভিডিওর মতো। জব কিউ, রিট্রাই আর আইডেম্পোটেন্সি VOD ট্র্যাকের চ্যাপ্টার ৪-এ।

লাইভ চলার মাঝে এনকোডার কয়েকবার কেটে গিয়ে থাকলে রেকর্ডিংয়ে EXT-X-DISCONTINUITY থাকবে, আর প্রতিটা অংশের টাইমস্ট্যাম্প আলাদা জায়গা থেকে শুরু। এমন রেকর্ডিং সরাসরি -c copy দিয়ে এক ফাইলে জোড়া দিলে টাইমস্ট্যাম্প লাফাতে পারে, অডিও-ভিডিও সরে যেতে পারে। নিরাপদ পথ হলো VOD পাইপলাইনে রি-এনকোড করা, যেখানে টাইমস্ট্যাম্প নতুন করে তৈরি হয়।

PROGRAM-DATE-TIME দিয়ে ক্লিপ কাটা

“সন্ধ্যা ৭টা ১২-এর উইকেটটা কেটে দাও” এই অনুরোধের উত্তর আসে PROGRAM-DATE-TIME থেকে। কাজটা দুই ধাপে: কোন সেগমেন্টগুলো ওই সময় ঢেকে রাখে সেটা খুঁজে বের করা, তারপর ওগুলো থেকে ঠিক অংশটুকু কাটা।

প্রথম ধাপের জন্য একটা ছোট Go প্রোগ্রাম, শুধু স্ট্যান্ডার্ড লাইব্রেরি দিয়ে:

// pdtclip prints the segments of an HLS media playlist that overlap a wall-clock range.
// Usage: pdtclip index.m3u8 2026-10-25T10:48:39+06:00 2026-10-25T10:48:44+06:00
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

// ffmpeg writes +0600, other packagers write +06:00 or Z
func parsePDT(v string) (time.Time, error) {
	if t, err := time.Parse("2006-01-02T15:04:05.000Z0700", v); err == nil {
		return t, nil
	}
	return time.Parse(time.RFC3339Nano, v)
}

func fail(code int, msg ...any) {
	fmt.Fprintln(os.Stderr, msg...)
	os.Exit(code)
}

func main() {
	if len(os.Args) != 4 {
		fail(2, "usage: pdtclip PLAYLIST FROM TO (RFC 3339 times)")
	}
	from, err1 := time.Parse(time.RFC3339, os.Args[2])
	to, err2 := time.Parse(time.RFC3339, os.Args[3])
	if err1 != nil || err2 != nil || !to.After(from) {
		fail(2, "FROM and TO must be RFC 3339 and TO after FROM")
	}
	f, err := os.Open(os.Args[1])
	if err != nil {
		fail(1, err)
	}
	defer f.Close()

	var dur float64
	var start time.Time
	found := 0
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		line := strings.TrimSpace(sc.Text())
		switch {
		case strings.HasPrefix(line, "#EXTINF:"):
			v := strings.Split(strings.TrimPrefix(line, "#EXTINF:"), ",")[0]
			if dur, err = strconv.ParseFloat(v, 64); err != nil {
				fail(1, "bad EXTINF:", line)
			}
		case strings.HasPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:"):
			if start, err = parsePDT(strings.TrimPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:")); err != nil {
				fail(1, "bad PROGRAM-DATE-TIME:", line)
			}
		case line != "" && !strings.HasPrefix(line, "#"):
			if start.IsZero() {
				fail(1, "segment without PROGRAM-DATE-TIME:", line)
			}
			end := start.Add(time.Duration(dur * float64(time.Second)))
			if start.Before(to) && end.After(from) {
				fmt.Printf("%s\t%s\t%.3f\n", line, start.Format(time.RFC3339Nano), dur)
				found++
			}
			// the next segment starts where this one ends unless a new PDT tag says otherwise
			start = end
		}
	}
	if err := sc.Err(); err != nil {
		fail(1, err)
	}
	if found == 0 {
		fail(1, "no segment overlaps that range")
	}
}

উপরের পরীক্ষামূলক EVENT প্লেলিস্টে চালিয়ে আসল আউটপুট:

$ go run . index.m3u8 2026-10-25T10:48:39+06:00 2026-10-25T10:48:44+06:00
seg_00001.ts	2026-10-25T10:48:37.246+06:00	2.000
seg_00002.ts	2026-10-25T10:48:39.246+06:00	2.000
seg_00003.ts	2026-10-25T10:48:41.246+06:00	2.000
seg_00004.ts	2026-10-25T10:48:43.246+06:00	2.000

$ go run . index.m3u8 2026-10-25T11:00:00+06:00 2026-10-25T11:01:00+06:00
no segment overlaps that range
exit status 1

seg_00001 তালিকায় এসেছে কারণ এটা ১০:৪৮:৩৯.২৪৬ পর্যন্ত চলে, মানে চাওয়া শুরুর (৩৯.০০০) একটু পরে শেষ হয়। দ্বিতীয় চালানোয় সময়টা রেকর্ডিংয়ের বাইরে, তাই প্রোগ্রাম স্পষ্ট বার্তা দিয়ে ব্যর্থ হয়েছে, খালি ক্লিপ বানায়নি।

দুই রকম কাটা: দ্রুত বনাম নির্ভুল

সেগমেন্ট পাওয়ার পর কাটার দুটো উপায়, আর পার্থক্যটা keyframe-এ। দুটোই একই ১২ সেকেন্ডের রেকর্ডিং থেকে ৩ থেকে ৯ সেকেন্ড, মানে ৬ সেকেন্ড চেয়ে আসলে চালানো:

# Fast: no re-encode, cuts snap to keyframes
ffmpeg -ss 3 -to 9 -i index.m3u8 -c copy clip_copy.mp4

# Exact: re-encode, cuts land on the requested frame
ffmpeg -i index.m3u8 -ss 3 -to 9 -c:v libx264 -preset veryfast -c:a aac clip_exact.mp4
clip_copy.mp4   duration 6.100 s   video packets 210 (7.0 s), first packet at -0.933 s, keyframe
clip_exact.mp4  duration 6.000 s

দ্রুত পথে ffmpeg ছবি নতুন করে বানাতে পারে না, তাই চাওয়া ৩ সেকেন্ডের আগের keyframe (২ সেকেন্ডে) থেকে শুরু করেছে। বাড়তি এক সেকেন্ডের ফ্রেমগুলো ফাইলে আছে, কিন্তু ঋণাত্মক সময়ে, “বাদ দাও” চিহ্ন দিয়ে লুকানো (MP4-এর edit list)। বেশিরভাগ প্লেয়ার এটা মেনে ৩ সেকেন্ড থেকে দেখায়, কিন্তু কিছু এডিটর বা সোশ্যাল প্ল্যাটফর্মে আপলোড করলে বাড়তি অংশটা দেখা যেতে পারে। নির্ভুল পথে ঠিক ৬ সেকেন্ড, কিন্তু রি-এনকোডে সময় আর CPU লাগে।

হাইলাইটের জন্য প্রচলিত সমাধান হলো দুটো মেশানো: আগে দ্রুত পথে একটা খসড়া ক্লিপ দিয়ে দিন যাতে সোশ্যাল মিডিয়ায় কয়েক সেকেন্ডে পোস্ট হয়, তারপর ব্যাকগ্রাউন্ডে নির্ভুল ক্লিপ বানিয়ে বদলে দিন। আর ২ সেকেন্ডের GOP হলে দ্রুত পথের ভুল সর্বোচ্চ ২ সেকেন্ড, যেটা হাইলাইটে প্রায়ই চলে যায়।

Catch-up: প্রোগ্রামের সময়সূচি থেকে কাটা

টিভি চ্যানেলের মতো “গত সাত দিনের যেকোনো অনুষ্ঠান” দিতে হলে একই কৌশল বড় আকারে: অনুষ্ঠানসূচিতে (EPG) প্রতিটা অনুষ্ঠানের শুরু আর শেষের সময় আছে, রেকর্ডিংয়ে প্রতিটা সেগমেন্টের PDT আছে। দুটো মিলিয়ে প্রতিটা অনুষ্ঠানের জন্য একটা প্লেলিস্ট বানানো যায় যেটা শুধু ওই সেগমেন্টগুলো দেখায়, কোনো ভিডিও কপি না করেই। অনুষ্ঠান দেরিতে শুরু হলে বা আগে শেষ হলে সূচির সময় ভুল হয়, তাই শুরু আর শেষে কয়েক মিনিট বাড়তি রাখা (padding) স্বাভাবিক।

রিটেনশন আর মুছে ফেলা

রেকর্ডিং জমতে থাকে, আর রাখার খরচ প্রতি মাসে বাড়ে। চালু করার দিনেই নিয়মটা লিখে ফেলুন:

কীকতদিন রাখবেন (উদাহরণ)কেন
লাইভ origin-এর DVR জানালাজানালার দৈর্ঘ্য (ঘণ্টা)শুধু চলমান দর্শকের জন্য
কাঁচা লাইভ রেকর্ডিংVOD-তে রি-এনকোড শেষ হওয়া পর্যন্ত, তারপর কয়েক দিনরি-এনকোড ব্যর্থ হলে আবার চালানো যায়
catch-up রেকর্ডিংcatch-up জানালা (যেমন সাত দিন)কন্টেন্টের অধিকারের চুক্তিতে যা বলা আছে
হাইলাইট ও স্থায়ী VODপ্রোডাক্টের সিদ্ধান্তএগুলো এখন VOD প্ল্যাটফর্মের দায়িত্ব

সংখ্যাগুলো উদাহরণ। আসল সংখ্যা আসে খরচ আর কন্টেন্টের অধিকার থেকে: অনেক খেলার সম্প্রচার চুক্তিতে নির্দিষ্ট থাকে কতদিন রিপ্লে দেখানো যাবে, তারপর মুছে ফেলা বাধ্যতামূলক।

মোছা স্বয়ংক্রিয় করুন। object storage-এর লাইফসাইকেল নিয়ম (S3 API-র lifecycle নিয়ম; Garage-এও Expiration সমর্থিত) দিয়ে একটা prefix-এর ফাইল নির্দিষ্ট দিন পর নিজে থেকে মুছে ফেলা যায়, কোনো cron স্ক্রিপ্ট ছাড়া। তাই রেকর্ডিংয়ের prefix রিটেনশন অনুযায়ী সাজান (catchup/, raw/), যাতে প্রতিটার আলাদা নিয়ম বসানো যায়। আর মনে রাখুন, storage থেকে মুছলেও CDN-এ কপি থেকে যেতে পারে; আইনি কারণে সরাতে হলে CDN থেকেও purge করুন (CDN ট্র্যাকের চ্যাপ্টার ৬)।

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

লক্ষণকীভাবে নিশ্চিত হবেনসমাধান
দর্শক কয়েক সেকেন্ডের বেশি পেছনে যেতে পারে নাপ্লেলিস্টে সেগমেন্টের সংখ্যা গুনুন; video.seekable রেঞ্জ দেখুনhls_list_size বাড়ান বা EVENT প্লেলিস্ট দিন
ম্যাচের শেষ দিকে মোবাইল দর্শক বেশি আটকাচ্ছেcurl -s URL -o p.m3u8 দিয়ে নামিয়ে আকার দেখুন; কমপ্রেশন পাচ্ছেন কিনা curl -sI -H 'Accept-Encoding: br, gzip' URL-এ content-encoding দেখে বুঝুনCDN-এ কমপ্রেশন চালু; লাইভ দর্শকের জন্য ছোট প্লেলিস্ট; ডেল্টা আপডেট সমর্থিত হলে চালু
মাঝপথে লাইভ থেমে গেল, ট্রান্সকোডার লগে “No space left on device”df -h লাইভ origin-এর ডিস্কেDVR জানালা ছোট করুন; রেকর্ডিং object storage-এ সরান; ডিস্ক মনিটরিং
রেকর্ডিংয়ে ফাঁক বা ভাঙা সেগমেন্টobject storage-এ সেগমেন্ট নম্বর ধারাবাহিক কিনা; ভাঙা ফাইলে ffprobe এরর দেয়temp_file ফ্ল্যাগ আর *.tmp বাদ দিয়ে আপলোড; আর্কাইভ ফোল্ডারে মোছা বন্ধ
“লাইভে ফিরুন” চাপলে সাথে সাথে বাফারিংবোতামটা seekable রেঞ্জের একেবারে শেষে পাঠাচ্ছেশেষ থেকে HOLD-BACK-এর সমান পেছনে পাঠান
পেছনে থাকা দর্শক নিজে থেকে দ্রুত চলে লাইভে চলে আসছেপ্লেয়ারের playback rate ১-এর বেশি, যদিও দর্শক নিজে সিক করেছিলদর্শক সিক করলে ধরে ফেলা বন্ধ রাখুন
হাইলাইট ক্লিপ চাওয়া সময়ের এক-দুই সেকেন্ড আগে শুরু হচ্ছেffprobe দিয়ে প্রথম প্যাকেটের সময় দেখুন; ঋণাত্মক হলে -c copy keyframe-এ কেটেছেনির্ভুল ক্লিপের জন্য রি-এনকোড; বা ছোট GOP
ক্লিপ সঠিক মুহূর্ত থেকে কয়েক সেকেন্ড সরে আছেট্রান্সকোডারের ঘড়ি: chronyc tracking বা timedatectlNTP চালু; সব মেশিন UTC-তে
রিমাক্স করা MP4-এ অডিও-ভিডিও সরে গেছেপ্লেলিস্টে EXT-X-DISCONTINUITY আছে কিনাdiscontinuity থাকলে VOD পাইপলাইনে রি-এনকোড
-c copy দিয়ে MP4 বানাতে AAC অডিও এররffmpeg এরর বলে “Malformed AAC bitstream”-bsf:a aac_adtstoasc যোগ করুন

মূল শিক্ষা

  1. DVR, start-over, live-to-VOD আর catch-up একই ভিত্তিতে দাঁড়ানো: সেগমেন্ট রেখে দেওয়া আর প্রতিটার বাস্তব সময় জানা।
  2. তিন রকম প্লেলিস্ট: লম্বা স্লাইডিং জানালা ২৪ ঘণ্টার চ্যানেলের জন্য, EVENT নির্দিষ্ট অনুষ্ঠানের জন্য, আর লাইভ দর্শকের জন্য আলাদা ছোট প্লেলিস্ট।
  3. লম্বা প্লেলিস্ট প্রতি রিফ্রেশে নামাতে হয়। তিন ঘণ্টায় প্রায় অর্ধ মেগাবাইট; CDN কমপ্রেশন একে কয়েক ভাগের এক ভাগে নামায়।
  4. স্টোরেজ বাড়ে সব রাংয়ের বিটরেটের যোগফলে। উদাহরণের ladder-এ প্রতি স্ট্রিম-ঘণ্টায় প্রায় ৪.২ GB; লাইভ ডিস্ক আর আর্কাইভ আলাদা রাখুন।
  5. “লাইভে ফিরুন” মানে শেষ থেকে একটু পেছনে, আর পেছনে থাকা দর্শককে জোর করে লাইভে টানবেন না।
  6. রেকর্ডিং মানে সম্পূর্ণ লেখা সেগমেন্ট object storage-এ তোলা, অস্থায়ী ফাইল বাদ দিয়ে, আর্কাইভে মোছা বন্ধ রেখে।
  7. শেষে ENDLIST, তারপর রিমাক্স বা VOD পাইপলাইনে রি-এনকোড। discontinuity থাকলে রি-এনকোডই নিরাপদ।
  8. ক্লিপ কাটা PDT দিয়ে। -c copy দ্রুত কিন্তু keyframe-এ কাটে; নির্ভুল কাটার জন্য রি-এনকোড।
  9. রিটেনশন প্রথম দিনেই লিখুন আর স্বয়ংক্রিয় করুন। লাইফসাইকেল নিয়ম, prefix ধরে, আর আইনি মোছায় CDN purge-ও।