Implementing HLS and DASH Video Streaming

Implementing HLS and DASH Video Streaming

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1414
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    982
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    995

Implementing HLS and DASH Video Streaming

You launch a video service, and after uploading the first videos, you realize — the player stutters, buffering never ends, and mobile users see nothing but a spinner. This is a familiar situation—we've encountered it many times. Our experience: 7+ years in video streaming. The solution is adaptive streaming with proper segmentation, CDN integration, and multi-resolution support. In this article, we'll explain how we implement an HLS/DASH pipeline turnkey, and the pitfalls we encountered.

HLS vs DASH: Which One to Choose?

HLS (HTTP Live Streaming) is an Apple standard, supported by all browsers via hls.js (except Safari, which has native support). DASH (Dynamic Adaptive Streaming over HTTP) is an open standard used for DRM compatibility. In practice, HLS is the default choice: lower latency, wider CDN support. DASH is needed when multiple DRM systems (Widevine, PlayReady) are required.

In our projects, we use HLS as the baseline and add DASH on demand. Comparison in the table:

Characteristic HLS DASH
Support All browsers (hls.js for non-Safari) All browsers (dash.js)
Container MPEG-TS or fMP4 fMP4
DRM FairPlay (Apple), Widevine Widevine, PlayReady
Latency 6–30 sec (LL-HLS: <2 sec) 2–10 sec
CDN compatibility Excellent Excellent

In our tests, HLS showed 2 times lower latency than DASH.

Why FFmpeg Is Not the Only Option?

For video transcoding, we use FFmpeg in conjunction with queues (Laravel Horizon), but for high volumes we incorporate AWS MediaConvert. Let's break down both approaches.

FFmpeg + Queue (Self-hosted)

The transcode-hls.sh script cuts video into three qualities: 1080p, 720p, 360p. Segments of 6 seconds (fMP4). Output: master.m3u8 and sub-playlists.

View full transcoding script ```bash #!/bin/bash # transcode-hls.sh

INPUT="$1" OUTPUT_DIR="$2"

mkdir -p "$OUTPUT_DIR"

ffmpeg -i "$INPUT" -filter_complex "[0:v]split=3[v1][v2][v3]; [v1]scale=1920:1080:force_original_aspect_ratio=decrease[v1080]; [v2]scale=1280:720:force_original_aspect_ratio=decrease[v720]; [v3]scale=640:360:force_original_aspect_ratio=decrease[v360]" -map "[v1080]" -map 0:a -c:v:0 libx264 -crf 22 -preset fast -b:v:0 5000k -maxrate:v:0 5500k -bufsize:v:0 10000k -c:a:0 aac -b:a:0 192k -map "[v720]" -map 0:a -c:v:1 libx264 -crf 23 -preset fast -b:v:1 2500k -maxrate:v:1 2750k -bufsize:v:1 5000k -c:a:1 aac -b:a:1 128k -map "[v360]" -map 0:a -c:v:2 libx264 -crf 24 -preset fast -b:v:2 800k -maxrate:v:2 880k -bufsize:v:2 1600k -c:a:2 aac -b:a:2 96k -f hls -hls_time 6 -hls_playlist_type vod -hls_flags independent_segments -hls_segment_type fmp4 -hls_segment_filename "$OUTPUT_DIR/v%v/seg%06d.m4s" -master_pl_name master.m3u8 -var_stream_map "v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:360p" "$OUTPUT_DIR/v%v/index.m3u8"

</details> Result: 

output/ ├── master.m3u8 ├── 1080p/ │ ├── index.m3u8 │ ├── seg000001.m4s │ └── ... ├── 720p/ │ └── ... └── 360p/ └── ...

 #### PHP Job for Queue The TranscodeToHlsJob class is a typical Laravel job. Timeout 2 hours, two attempts. We only handle the successful case. ```php class TranscodeToHlsJob implements ShouldQueue { public int $timeout = 7200; public int $tries = 2; public function __construct(private Video $video) {} public function handle(): void { $this->video->update(['status' => 'transcoding']); $inputUrl = Storage::disk('s3')->temporaryUrl($this->video->original_key, now()->addHours(3)); $outputDir = sys_get_temp_dir() . '/hls_' . $this->video->id; $s3Prefix = "hls/{$this->video->user_id}/{$this->video->id}"; mkdir($outputDir, 0777, true); $process = new \Symfony\Component\Process\Process([ '/usr/local/bin/transcode-hls.sh', $inputUrl, $outputDir ]); $process->setTimeout(7200); $process->run(); if (!$process->isSuccessful()) { throw new \RuntimeException('HLS transcoding failed: ' . $process->getErrorOutput()); } // Upload all files to S3 $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($outputDir) ); foreach ($iterator as $file) { if (!$file->isFile()) continue; $relativePath = str_replace($outputDir . '/', '', $file->getPathname()); $s3Key = "{$s3Prefix}/{$relativePath}"; $contentType = str_ends_with($file->getFilename(), '.m3u8') ? 'application/x-mpegURL' : 'video/mp4'; Storage::disk('s3')->put($s3Key, file_get_contents($file->getPathname()), [ 'ContentType' => $contentType, 'CacheControl' => str_ends_with($file->getFilename(), '.m3u8') ? 'no-cache' // playlist not cached : 'public, max-age=31536000', // segments cached forever ]); } $this->video->update([ 'status' => 'ready', 'hls_manifest' => "{$s3Prefix}/master.m3u8", ]); // Clean up temporary files exec("rm -rf {$outputDir}"); } } 

AWS MediaConvert (Managed)

For high-load projects, we use MediaConvert. Configuration in Python:

def create_hls_job(input_key: str, output_prefix: str) -> str: client = boto3.client('mediaconvert', endpoint_url=MEDIACONVERT_ENDPOINT) job = client.create_job( Role=MEDIACONVERT_ROLE, Settings={ 'Inputs': [{'FileInput': f's3://bucket/{input_key}', 'VideoSelector': {}, 'AudioSelectors': {'Audio 1': {'DefaultSelection': 'DEFAULT'}}}], 'OutputGroups': [{ 'Name': 'Apple HLS', 'OutputGroupSettings': { 'Type': 'HLS_GROUP_SETTINGS', 'HlsGroupSettings': { 'Destination': f's3://bucket/{output_prefix}/', 'SegmentLength': 6, 'MinSegmentLength': 0, 'DirectoryStructure': 'SUBDIRECTORY_PER_STREAM', }, }, 'Outputs': [ {'NameModifier': '_1080p', 'VideoDescription': {'Width': 1920, 'Height': 1080, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 5000000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 192000}}}]}, {'NameModifier': '_720p', 'VideoDescription': {'Width': 1280, 'Height': 720, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 2500000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 128000}}}]}, {'NameModifier': '_360p', 'VideoDescription': {'Width': 640, 'Height': 360, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 800000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 96000}}}]}, ], }], } ) return job['Job']['Id'] 

MediaConvert generates HLS playlists on its own. We pay only for transcoding minutes.

CloudFront CDN

To ensure fast video delivery worldwide, we configure CloudFront with two cache policies: for playlists (TTL 5 seconds) and segments (TTL 1 year).

resource "aws_cloudfront_distribution" "video" { origin { domain_name = aws_s3_bucket.videos.bucket_regional_domain_name origin_id = "s3-videos" origin_access_control_id = aws_cloudfront_origin_access_control.default.id } enabled = true is_ipv6_enabled = true default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = "s3-videos" viewer_protocol_policy = "redirect-to-https" forwarded_values { query_string = false cookies { forward = "none" } headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"] } min_ttl = 0 default_ttl = 86400 max_ttl = 31536000 } # Playlists .m3u8 — short cache ordered_cache_behavior { path_pattern = "*.m3u8" allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = "s3-videos" viewer_protocol_policy = "redirect-to-https" forwarded_values { query_string = false cookies { forward = "none" } } min_ttl = 0 default_ttl = 5 max_ttl = 30 } } 

React Player with hls.js

Client-side integration: hls.js for modern browsers, fallback to native HLS in Safari.

import Hls from 'hls.js'; import { useEffect, useRef } from 'react'; interface VideoPlayerProps { manifestUrl: string; poster?: string; } export function VideoPlayer({ manifestUrl, poster }: VideoPlayerProps) { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { const video = videoRef.current; if (!video) return; if (Hls.isSupported()) { const hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, lowLatencyMode: false, }); hls.loadSource(manifestUrl); hls.attachMedia(video); hls.on(Hls.Events.ERROR, (_, data) => { if (data.fatal) { if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { hls.startLoad(); } else { hls.destroy(); } } }); return () => hls.destroy(); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari: native HLS support video.src = manifestUrl; } }, [manifestUrl]); return ( <video ref={videoRef} poster={poster} controls playsInline style={{ width: '100%', maxHeight: '80vh' }} /> ); } 

What's Included in the Work

We handle the complete cycle for implementing HLS/DASH streaming on your website:

  • Transcoding video into three qualities (1080p, 720p, 360p) using FFmpeg or AWS MediaConvert.
  • Integration with queues (Laravel Horizon) for async processing.
  • Development of a custom React player based on hls.js with UI customization support.
  • Setting up CDN (CloudFront or Cloudflare) for fast content delivery.
  • Detailed documentation of the pipeline architecture.
  • Training your team on the system.
  • 2 weeks of post-launch support.

Typical Mistakes

  • Incorrect bitrates: too low for 1080p, too high for 360p. We use Apple recommendations.
  • Playlist caching: if master.m3u8 is cached, the player won't see new qualities after retranscoding. We set no-cache.
  • Missing CORS: browser blocks segments. We configure CORS on S3/CloudFront.
  • Hydration mismatch: if the player renders on the server, it may mismatch with client. We use dynamic import.

Process of Work

  1. Analysis — study source videos, DRM requirements, target audience.
  2. Design — select stack (self-hosted FFmpeg or MediaConvert), determine bitrates, CDN.
  3. Implementation — write transcoder, job, player.
  4. Testing — check LCP, CLS, INP, Core Web Vitals.
  5. Deployment — deploy to production, monitor logs.

Timeline and Cost

Step Timeline
FFmpeg HLS transcoding in queue 4–5 days
AWS MediaConvert integration 2–3 days
hls.js player with React 1–2 days
CloudFront CDN delivery 1–2 days
Full pipeline (upload → transcode → player) 7–14 days

Cost is calculated individually based on video volume, bitrates, and DRM needs. Contact us for an assessment of your project — we'll calculate the timeline and cost. Order HLS/DASH streaming implementation and get a consultation. We guarantee your video will work lag-free on any device.