Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. IPTV Stream Validation and M3U Playlist Management with
IPTV Stream Validation and M3U Playlist Management with
HOWTOIntermediate

IPTV Stream Validation and M3U Playlist Management with

Build a robust IPTV stream management system using FFmpeg for validation, M3U playlist parsing, EPG integration, and automated health monitoring of live...

Dylan H.

Software Engineering

February 6, 2026
5 min read

Prerequisites

  • FFmpeg installed
  • Node.js 18+
  • Basic understanding of streaming protocols

Overview

Managing IPTV streams requires validating that channels are online, parsing M3U playlists, and detecting duplicates. This guide builds a management system using FFmpeg for stream probing and Next.js for the interface.

Key Capabilities

  • Parse M3U/M3U8 playlist files with metadata extraction
  • Validate streams using FFmpeg probe (codec, resolution, bitrate)
  • Detect duplicate channels across multiple playlists
  • Automated health monitoring with configurable intervals

Requirements

ComponentPurpose
FFmpegStream probing and validation
Node.js 18+Runtime
Next.js 15+Web interface
# Install FFmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
 
# macOS
brew install ffmpeg
 
# Verify installation
ffmpeg -version

Process

Step 1: M3U Playlist Parser

M3U playlists use the #EXTINF directive for metadata:

// lib/m3u-parser.ts
 
interface Channel {
  id: string;
  name: string;
  url: string;
  group: string;
  logo?: string;
  language?: string;
  tvgId?: string;
}
 
export function parseM3U(content: string): Channel[] {
  const lines = content.split("\n").map((l) => l.trim());
  const channels: Channel[] = [];
  let currentMeta: Partial<Channel> = {};
 
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
 
    if (line.startsWith("#EXTINF")) {
      // Parse metadata: #EXTINF:-1 tvg-id="..." group-title="..." tvg-logo="...",Channel Name
      const attrs: Record<string, string> = {};
      const attrRegex = /(\w[\w-]*)="([^"]*)"/g;
      let match;
      while ((match = attrRegex.exec(line)) !== null) {
        attrs[match[1]] = match[2];
      }
 
      // Channel name is after the last comma
      const nameMatch = line.match(/,(.+)$/);
 
      currentMeta = {
        name: nameMatch?.[1]?.trim() || "Unknown",
        group: attrs["group-title"] || "Uncategorized",
        logo: attrs["tvg-logo"],
        tvgId: attrs["tvg-id"],
        language: attrs["tvg-language"],
      };
    } else if (line.startsWith("http") && currentMeta.name) {
      channels.push({
        id: generateId(currentMeta.name, line),
        url: line,
        ...currentMeta,
      } as Channel);
      currentMeta = {};
    }
  }
 
  return channels;
}
 
function generateId(name: string, url: string): string {
  const hash = Buffer.from(`${name}:${url}`).toString("base64url").slice(0, 12);
  return hash;
}

Step 2: FFmpeg Stream Validation

// lib/stream-validator.ts
import { execFile } from "child_process";
import { promisify } from "util";
 
const execFileAsync = promisify(execFile);
 
interface StreamInfo {
  valid: boolean;
  codec?: string;
  resolution?: string;
  bitrate?: number;
  fps?: number;
  error?: string;
  responseTimeMs: number;
}
 
export async function validateStream(
  url: string,
  timeoutSec: number = 10
): Promise<StreamInfo> {
  const startTime = Date.now();
 
  try {
    const { stdout } = await execFileAsync("ffprobe", [
      "-v", "quiet",
      "-print_format", "json",
      "-show_streams",
      "-show_format",
      "-timeout", String(timeoutSec * 1000000), // microseconds
      url,
    ], { timeout: (timeoutSec + 5) * 1000 });
 
    const data = JSON.parse(stdout);
    const videoStream = data.streams?.find(
      (s: any) => s.codec_type === "video"
    );
 
    return {
      valid: true,
      codec: videoStream?.codec_name,
      resolution: videoStream
        ? `${videoStream.width}x${videoStream.height}`
        : undefined,
      bitrate: data.format?.bit_rate
        ? parseInt(data.format.bit_rate)
        : undefined,
      fps: videoStream?.r_frame_rate
        ? eval(videoStream.r_frame_rate)
        : undefined,
      responseTimeMs: Date.now() - startTime,
    };
  } catch (err: any) {
    return {
      valid: false,
      error: err.message?.slice(0, 200),
      responseTimeMs: Date.now() - startTime,
    };
  }
}

Step 3: Duplicate Detection

// lib/duplicate-detector.ts
import { Channel } from "./m3u-parser";
 
interface DuplicateGroup {
  channels: Channel[];
  matchType: "exact-url" | "similar-name" | "same-tvg-id";
}
 
export function findDuplicates(channels: Channel[]): DuplicateGroup[] {
  const duplicates: DuplicateGroup[] = [];
 
  // 1. Exact URL matches
  const urlMap = new Map<string, Channel[]>();
  for (const ch of channels) {
    const normalized = ch.url.replace(/\?.+$/, ""); // Strip query params
    const existing = urlMap.get(normalized) || [];
    existing.push(ch);
    urlMap.set(normalized, existing);
  }
  for (const group of urlMap.values()) {
    if (group.length > 1) {
      duplicates.push({ channels: group, matchType: "exact-url" });
    }
  }
 
  // 2. Same TVG-ID
  const tvgMap = new Map<string, Channel[]>();
  for (const ch of channels) {
    if (!ch.tvgId) continue;
    const existing = tvgMap.get(ch.tvgId) || [];
    existing.push(ch);
    tvgMap.set(ch.tvgId, existing);
  }
  for (const group of tvgMap.values()) {
    if (group.length > 1) {
      duplicates.push({ channels: group, matchType: "same-tvg-id" });
    }
  }
 
  // 3. Similar names (Jaccard similarity)
  const nameGroups = findSimilarNames(channels, 0.7);
  for (const group of nameGroups) {
    if (group.length > 1) {
      duplicates.push({ channels: group, matchType: "similar-name" });
    }
  }
 
  return duplicates;
}
 
function findSimilarNames(channels: Channel[], threshold: number): Channel[][] {
  const groups: Channel[][] = [];
  const used = new Set<number>();
 
  for (let i = 0; i < channels.length; i++) {
    if (used.has(i)) continue;
    const group = [channels[i]];
 
    for (let j = i + 1; j < channels.length; j++) {
      if (used.has(j)) continue;
      const similarity = jaccardSimilarity(
        channels[i].name.toLowerCase(),
        channels[j].name.toLowerCase()
      );
      if (similarity >= threshold) {
        group.push(channels[j]);
        used.add(j);
      }
    }
 
    if (group.length > 1) {
      used.add(i);
      groups.push(group);
    }
  }
 
  return groups;
}
 
function jaccardSimilarity(a: string, b: string): number {
  const setA = new Set(a.split(/\s+/));
  const setB = new Set(b.split(/\s+/));
  const intersection = new Set([...setA].filter((x) => setB.has(x)));
  const union = new Set([...setA, ...setB]);
  return intersection.size / union.size;
}

Step 4: Health Monitor

// lib/health-monitor.ts
import { validateStream } from "./stream-validator";
 
interface HealthResult {
  channelId: string;
  url: string;
  status: "online" | "offline" | "degraded";
  responseTimeMs: number;
  checkedAt: string;
}
 
export async function checkHealth(
  channels: { id: string; url: string }[],
  concurrency: number = 5
): Promise<HealthResult[]> {
  const results: HealthResult[] = [];
 
  // Process in batches to avoid overwhelming the network
  for (let i = 0; i < channels.length; i += concurrency) {
    const batch = channels.slice(i, i + concurrency);
    const batchResults = await Promise.all(
      batch.map(async (ch) => {
        const info = await validateStream(ch.url, 8);
        return {
          channelId: ch.id,
          url: ch.url,
          status: info.valid
            ? info.responseTimeMs > 5000 ? "degraded" as const : "online" as const
            : "offline" as const,
          responseTimeMs: info.responseTimeMs,
          checkedAt: new Date().toISOString(),
        };
      })
    );
    results.push(...batchResults);
  }
 
  return results;
}

Stream Quality Tiers

TierResolutionBitrateUse Case
SD720x4801-2 MbpsMobile, low bandwidth
HD1280x7202-5 MbpsStandard viewing
FHD1920x10805-10 MbpsDesktop, smart TV
4K3840x216015-25 MbpsLarge displays

Key Takeaways

  • FFprobe (ffprobe) is more reliable than HTTP HEAD checks for stream validation
  • Batch concurrent validations (5-10 at a time) to avoid network saturation
  • Jaccard similarity catches duplicate channels with slightly different names
  • Store validation results for trend analysis — streams often degrade before failing
  • Always set timeouts on FFmpeg operations to prevent hanging processes
#IPTV#FFmpeg#Streaming#M3U#Next.js

Related Articles

Building Offline-First PWAs with Next.js and SQLite

Learn how to build a Progressive Web App with offline-first architecture using Next.js, SQLite for local storage, and Supabase for cloud sync. Includes...

5 min read

Building Desktop Apps with Electron and Next.js

Create cross-platform desktop applications by combining Electron for native capabilities with Next.js for the UI. Covers IPC communication, system tray,...

5 min read

Building a Content Platform with Next.js 16 and

Create a full-featured technical blog platform using Next.js 16 App Router and Contentlayer2 for MDX content management. Covers document types, search,...

5 min read
Back to all HOWTOs