package streaming import ( "fmt" "mime" "path/filepath" "strconv" "strings" ) const ( // HLS segment duration in seconds DefaultSegmentDuration = 6.0 // Target segment size in bytes (approximately) DefaultTargetSegmentSize = 2 * 1024 * 1024 // 2MB to match our chunk size ) type HLSConfig struct { SegmentDuration float64 TargetSegmentSize int64 PlaylistType string // VOD or LIVE AllowCache bool Version int } type MediaSegment struct { Index int Duration float64 Size int64 ChunkIndexes []int // Which chunks make up this segment URI string } type HLSPlaylist struct { Config HLSConfig Segments []MediaSegment TotalDuration float64 TargetDuration float64 MediaSequence int EndList bool } // QualityLevel represents a transcoded quality level for HLS type QualityLevel struct { Name string Width int Height int Bitrate string Bandwidth int // bits per second for HLS Codecs string // codec string for HLS Resolution string // WxH format FileHash string // hash of transcoded file } // MasterPlaylist represents an HLS master playlist with multiple quality levels type MasterPlaylist struct { Qualities []QualityLevel BaseURL string } // DefaultQualityLevels returns standard quality levels for HLS func DefaultQualityLevels() []QualityLevel { return []QualityLevel{ { Name: "1080p", Width: 1920, Height: 1080, Bitrate: "5000k", Bandwidth: 5000000, Codecs: "avc1.640028,mp4a.40.2", Resolution: "1920x1080", }, { Name: "720p", Width: 1280, Height: 720, Bitrate: "2500k", Bandwidth: 2500000, Codecs: "avc1.64001f,mp4a.40.2", Resolution: "1280x720", }, { Name: "480p", Width: 854, Height: 480, Bitrate: "1000k", Bandwidth: 1000000, Codecs: "avc1.64001e,mp4a.40.2", Resolution: "854x480", }, } } type FileInfo struct { Name string Size int64 ChunkCount int ChunkSize int Duration float64 // For video files, estimated duration IsVideo bool MimeType string } // DefaultHLSConfig returns default HLS configuration func DefaultHLSConfig() HLSConfig { return HLSConfig{ SegmentDuration: DefaultSegmentDuration, TargetSegmentSize: DefaultTargetSegmentSize, PlaylistType: "VOD", AllowCache: true, Version: 3, } } // DetectMediaType determines if a file is a video and its MIME type func DetectMediaType(filename string) (bool, string) { ext := strings.ToLower(filepath.Ext(filename)) mimeType := mime.TypeByExtension(ext) videoExtensions := map[string]bool{ ".mp4": true, ".mkv": true, ".avi": true, ".mov": true, ".wmv": true, ".flv": true, ".webm": true, ".m4v": true, ".3gp": true, ".ts": true, } isVideo := videoExtensions[ext] if mimeType == "" { if isVideo { // Default MIME type for unknown video extensions mimeType = "video/mp4" } else { mimeType = "application/octet-stream" } } return isVideo, mimeType } // EstimateVideoDuration provides a rough estimation of video duration based on file size // This is a simple heuristic - in production you'd use ffprobe or similar func EstimateVideoDuration(fileSize int64, filename string) float64 { // Very rough estimation: assume different bitrates based on file extension ext := strings.ToLower(filepath.Ext(filename)) var estimatedBitrate int64 // bits per second switch ext { case ".mp4", ".m4v": estimatedBitrate = 2000000 // 2 Mbps average case ".mkv": estimatedBitrate = 3000000 // 3 Mbps average case ".avi": estimatedBitrate = 1500000 // 1.5 Mbps average case ".webm": estimatedBitrate = 1000000 // 1 Mbps average default: estimatedBitrate = 2000000 // Default 2 Mbps } // Duration = (file size in bits) / bitrate fileSizeInBits := fileSize * 8 duration := float64(fileSizeInBits) / float64(estimatedBitrate) // Ensure minimum duration of 10 seconds for very small files if duration < 10.0 { duration = 10.0 } return duration } // GenerateHLSSegments creates HLS segments from file chunks func GenerateHLSSegments(fileInfo FileInfo, config HLSConfig) (*HLSPlaylist, error) { if !fileInfo.IsVideo { return nil, fmt.Errorf("file is not a video: %s", fileInfo.Name) } playlist := &HLSPlaylist{ Config: config, Segments: make([]MediaSegment, 0), MediaSequence: 0, EndList: true, // VOD content } // Calculate number of segments based on duration and target segment duration totalSegments := int(fileInfo.Duration/config.SegmentDuration) + 1 if totalSegments < 1 { totalSegments = 1 } segmentDuration := fileInfo.Duration / float64(totalSegments) playlist.TargetDuration = segmentDuration // Calculate chunks per segment chunksPerSegment := fileInfo.ChunkCount / totalSegments if chunksPerSegment < 1 { chunksPerSegment = 1 } // Generate segments for i := 0; i < totalSegments; i++ { startChunk := i * chunksPerSegment endChunk := startChunk + chunksPerSegment // Handle last segment if i == totalSegments-1 { endChunk = fileInfo.ChunkCount } // Ensure we don't exceed chunk count if endChunk > fileInfo.ChunkCount { endChunk = fileInfo.ChunkCount } chunkIndexes := make([]int, 0) for j := startChunk; j < endChunk; j++ { chunkIndexes = append(chunkIndexes, j) } segmentSize := int64(len(chunkIndexes)) * int64(fileInfo.ChunkSize) segment := MediaSegment{ Index: i, Duration: segmentDuration, Size: segmentSize, ChunkIndexes: chunkIndexes, URI: fmt.Sprintf("segment_%d.ts", i), } playlist.Segments = append(playlist.Segments, segment) } playlist.TotalDuration = fileInfo.Duration return playlist, nil } // GenerateM3U8Manifest creates the HLS playlist manifest func (p *HLSPlaylist) GenerateM3U8Manifest(baseURL string) string { var builder strings.Builder // Header builder.WriteString("#EXTM3U\n") builder.WriteString(fmt.Sprintf("#EXT-X-VERSION:%d\n", p.Config.Version)) builder.WriteString(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", int(p.TargetDuration)+1)) builder.WriteString(fmt.Sprintf("#EXT-X-MEDIA-SEQUENCE:%d\n", p.MediaSequence)) builder.WriteString(fmt.Sprintf("#EXT-X-PLAYLIST-TYPE:%s\n", p.Config.PlaylistType)) if !p.Config.AllowCache { builder.WriteString("#EXT-X-ALLOW-CACHE:NO\n") } // Segments for _, segment := range p.Segments { builder.WriteString(fmt.Sprintf("#EXTINF:%.3f,\n", segment.Duration)) segmentURL := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), segment.URI) builder.WriteString(segmentURL + "\n") } // End marker for VOD if p.EndList { builder.WriteString("#EXT-X-ENDLIST\n") } return builder.String() } // GetSegmentByIndex returns a segment by its index func (p *HLSPlaylist) GetSegmentByIndex(index int) (*MediaSegment, error) { if index < 0 || index >= len(p.Segments) { return nil, fmt.Errorf("segment index %d out of range (0-%d)", index, len(p.Segments)-1) } return &p.Segments[index], nil } // GetSegmentByURI returns a segment by its URI func (p *HLSPlaylist) GetSegmentByURI(uri string) (*MediaSegment, error) { for _, segment := range p.Segments { if segment.URI == uri { return &segment, nil } } return nil, fmt.Errorf("segment not found: %s", uri) } // ParseSegmentURI extracts segment index from URI like "segment_0.ts" func ParseSegmentURI(uri string) (int, error) { // Remove extension name := strings.TrimSuffix(uri, filepath.Ext(uri)) // Extract number from "segment_N" format parts := strings.Split(name, "_") if len(parts) != 2 || parts[0] != "segment" { return 0, fmt.Errorf("invalid segment URI format: %s", uri) } index, err := strconv.Atoi(parts[1]) if err != nil { return 0, fmt.Errorf("invalid segment index in URI %s: %v", uri, err) } return index, nil } // RangeRequest represents an HTTP range request type RangeRequest struct { Start int64 End int64 Size int64 } // ParseRangeHeader parses HTTP Range header like "bytes=0-1023" func ParseRangeHeader(rangeHeader string, fileSize int64) (*RangeRequest, error) { if rangeHeader == "" { return nil, nil } // Remove "bytes=" prefix if !strings.HasPrefix(rangeHeader, "bytes=") { return nil, fmt.Errorf("invalid range header format: %s", rangeHeader) } rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=") // Handle different range formats if strings.Contains(rangeSpec, ",") { // Multiple ranges not supported for simplicity return nil, fmt.Errorf("multiple ranges not supported") } parts := strings.Split(rangeSpec, "-") if len(parts) != 2 { return nil, fmt.Errorf("invalid range format: %s", rangeSpec) } var start, end int64 var err error // Parse start if parts[0] != "" { start, err = strconv.ParseInt(parts[0], 10, 64) if err != nil { return nil, fmt.Errorf("invalid start range: %v", err) } } // Parse end if parts[1] != "" { end, err = strconv.ParseInt(parts[1], 10, 64) if err != nil { return nil, fmt.Errorf("invalid end range: %v", err) } } else { // If no end specified, use file size - 1 end = fileSize - 1 } // Handle suffix-byte-range-spec (e.g., "-500" means last 500 bytes) if parts[0] == "" { start = fileSize - end end = fileSize - 1 } // Validate range if start < 0 { start = 0 } if end >= fileSize { end = fileSize - 1 } if start > end { return nil, fmt.Errorf("invalid range: start %d > end %d", start, end) } return &RangeRequest{ Start: start, End: end, Size: end - start + 1, }, nil } // FormatContentRange formats the Content-Range header func (r *RangeRequest) FormatContentRange(fileSize int64) string { return fmt.Sprintf("bytes %d-%d/%d", r.Start, r.End, fileSize) } // ChunkRange represents which chunks and byte offsets are needed for a range request type ChunkRange struct { StartChunk int EndChunk int StartOffset int64 // Byte offset within start chunk EndOffset int64 // Byte offset within end chunk TotalBytes int64 } // CalculateChunkRange determines which chunks are needed for a byte range func CalculateChunkRange(rangeReq *RangeRequest, chunkSize int) *ChunkRange { startChunk := int(rangeReq.Start / int64(chunkSize)) endChunk := int(rangeReq.End / int64(chunkSize)) startOffset := rangeReq.Start % int64(chunkSize) endOffset := rangeReq.End % int64(chunkSize) return &ChunkRange{ StartChunk: startChunk, EndChunk: endChunk, StartOffset: startOffset, EndOffset: endOffset, TotalBytes: rangeReq.Size, } } // GenerateMasterPlaylist creates an HLS master playlist for adaptive bitrate streaming func (mp *MasterPlaylist) GenerateMasterPlaylist() string { var builder strings.Builder // Header builder.WriteString("#EXTM3U\n") builder.WriteString("#EXT-X-VERSION:6\n") // Stream information for each quality for _, quality := range mp.Qualities { // EXT-X-STREAM-INF line with bandwidth, resolution, and codecs builder.WriteString(fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%s,CODECS=\"%s\"\n", quality.Bandwidth, quality.Resolution, quality.Codecs)) // Playlist URL for this quality playlistURL := fmt.Sprintf("%s/%s.m3u8", strings.TrimSuffix(mp.BaseURL, "/"), quality.Name) builder.WriteString(playlistURL + "\n") } return builder.String() } // CreateHLSForQuality generates HLS segments and playlist for a specific quality func CreateHLSForQuality(fileInfo FileInfo, config HLSConfig, qualityLevel QualityLevel, baseURL string) (*HLSPlaylist, error) { if !fileInfo.IsVideo { return nil, fmt.Errorf("file is not a video: %s", fileInfo.Name) } // Update file info with quality-specific hash if available if qualityLevel.FileHash != "" { fileInfo.Name = fmt.Sprintf("%s_%s", fileInfo.Name, qualityLevel.Name) } // Generate standard HLS playlist playlist, err := GenerateHLSSegments(fileInfo, config) if err != nil { return nil, err } // Update segment URIs to include quality prefix for i := range playlist.Segments { playlist.Segments[i].URI = fmt.Sprintf("%s_segment_%d.ts", qualityLevel.Name, playlist.Segments[i].Index) } return playlist, nil } // GenerateMultiQualityHLS creates HLS playlists for multiple quality levels func GenerateMultiQualityHLS(fileInfo FileInfo, config HLSConfig, qualityLevels []QualityLevel, baseURL string) (*MasterPlaylist, map[string]*HLSPlaylist, error) { if !fileInfo.IsVideo { return nil, nil, fmt.Errorf("file is not a video: %s", fileInfo.Name) } masterPlaylist := &MasterPlaylist{ Qualities: qualityLevels, BaseURL: baseURL, } qualityPlaylists := make(map[string]*HLSPlaylist) // Generate playlist for each quality for _, quality := range qualityLevels { playlist, err := CreateHLSForQuality(fileInfo, config, quality, baseURL) if err != nil { return nil, nil, fmt.Errorf("failed to create HLS for quality %s: %w", quality.Name, err) } qualityPlaylists[quality.Name] = playlist } return masterPlaylist, qualityPlaylists, nil }