├── hls ├── hls_test.go └── hls.go ├── mediachunk ├── mediachunk_test.go └── mediachunk.go ├── obsstudio.png ├── fixture └── testSmall.ts ├── pics └── blockDiagramGoSegmenter.png ├── go.mod ├── test_ffmpeg.sh ├── .gitignore ├── inpipe ├── inpipe.go └── intcp.go ├── README.md ├── LICENSE ├── logger └── logger.go ├── httpserver ├── httpserver.go └── nocache.go ├── go.sum ├── tspacket ├── tspacket_test.go └── tspacket.go ├── scripts └── multi-rendition.sh ├── main.go ├── logs └── segmenter.log └── manifestgenerator ├── manifestgenerator_test.go └── manifestgenerator.go /hls/hls_test.go: -------------------------------------------------------------------------------- 1 | package hls 2 | -------------------------------------------------------------------------------- /mediachunk/mediachunk_test.go: -------------------------------------------------------------------------------- 1 | package mediachunk 2 | 3 | //TODO: Add testing 4 | -------------------------------------------------------------------------------- /obsstudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/covrom/hls-streamer/HEAD/obsstudio.png -------------------------------------------------------------------------------- /fixture/testSmall.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/covrom/hls-streamer/HEAD/fixture/testSmall.ts -------------------------------------------------------------------------------- /pics/blockDiagramGoSegmenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/covrom/hls-streamer/HEAD/pics/blockDiagramGoSegmenter.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/covrom/hls-streamer 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 7 | github.com/natefinch/lumberjack v2.0.0+incompatible 8 | github.com/sirupsen/logrus v1.4.2 9 | golang.org/x/sys v0.1.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /test_ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ffmpeg -f lavfi -re -i smptebars=duration=6000:size=320x200:rate=30 -f lavfi -i sine=frequency=1000:duration=6000:sample_rate=48000 -pix_fmt yuv420p -c:v libx264 -b:v 180k -g 60 -keyint_min 60 -profile:v baseline -preset veryfast -c:a aac -b:a 96k -f mpegts tcp://127.0.0.1:9555 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | hls-streamer 14 | 15 | # server logs 16 | logs/ 17 | 18 | # segment results 19 | results/ -------------------------------------------------------------------------------- /inpipe/inpipe.go: -------------------------------------------------------------------------------- 1 | package inpipe 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | 8 | "github.com/covrom/hls-streamer/manifestgenerator" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func InPipe(readBufferSize int, mg *manifestgenerator.ManifestGenerator, log *logrus.Logger) { 14 | buf := make([]byte, 0, readBufferSize) 15 | r := bufio.NewReader(os.Stdin) 16 | // Buffer 17 | for { 18 | n, err := r.Read(buf[:cap(buf)]) 19 | if n == 0 && err == io.EOF { 20 | // Detected EOF 21 | // Closing 22 | log.Info("Closing process detected EOF") 23 | break 24 | } 25 | 26 | if err != nil && err != io.EOF { 27 | // Error reading pipe 28 | log.Fatal(err) 29 | os.Exit(1) 30 | } 31 | 32 | // process buf 33 | log.Debug("Sent to process: ", n, " bytes") 34 | mg.AddData(buf[:n]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hls-streamer 2 | 3 | Build and run ./hls-streamer without parameters. 4 | The http server with the player starts, as well as the TCP server for receiving the stream from OBS Studio. 5 | 6 | OBS Studio is configured like this: 7 | 8 | ![OBSStudio](./obsstudio.png) 9 | 10 | ## Usage 11 | 12 | 1. Build and run 13 | 2. Run OBS Studio with settings above 14 | 4. Push on "Start Recording" (not streaming!) in OBS Studio 15 | 5. Open browser at http://localhost:9099 16 | 6. See translation from OBS Studio with time delay about 6-10 second 17 | 18 | ## Architecture 19 | 20 | - OBS Studio -> hls-streamer with one-input access TCP stream from OBS Studio, or system stdin pipe (see --help), 21 | - hls-streamer -> m3u8 and HLS chunks at file system, 22 | - http server sharing m3u8 and HLS chunks (http.FileServer), 23 | - index page with video.js, that render streaming. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roman Сovanyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/natefinch/lumberjack" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | const timestampFormat = "2006-01-02 15:04:05.001 -0700 MST" 12 | 13 | // ConfigureLogger init log settings 14 | func ConfigureLogger(verbose bool) *logrus.Logger { 15 | var log = logrus.New() 16 | // default level is info 17 | if verbose { 18 | log.SetLevel(logrus.DebugLevel) 19 | } 20 | multiWriter := io.MultiWriter(os.Stderr, &lumberjack.Logger{ 21 | Filename: "logs/server.log", // Filename is the file to write logs to. Backup log files will be retained in the same directory. 22 | MaxSize: 50, // MaxSize is the maximum size in megabytes of the log file before it gets rotated 23 | MaxBackups: 5, // MaxBackups is the maximum number of old log files to retain. 24 | MaxAge: 30, // MaxAge is the maximum number of days to retain old log files based on the timestamp encoded in their filename. 25 | }) 26 | log.SetOutput(multiWriter) 27 | 28 | dateFormatter := &logrus.JSONFormatter{ 29 | TimestampFormat: timestampFormat, 30 | } 31 | // output in JSON format 32 | log.SetFormatter(dateFormatter) 33 | 34 | return log 35 | } 36 | -------------------------------------------------------------------------------- /httpserver/httpserver.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func HTTPServer(baseOutPath, chunkListFilename, serveHttpAddr string, log *logrus.Logger) { 10 | fs := http.FileServer(http.Dir(baseOutPath)) 11 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 12 | w.Write([]byte(` 13 | 14 | 15 | 16 | 17 | 18 | 19 | 35 | 36 | `)) 37 | }) 38 | http.Handle("/video/", NoCache(http.StripPrefix("/video/", fs))) 39 | 40 | go http.ListenAndServe(serveHttpAddr, nil) 41 | 42 | log.Printf("HTTP server listening on %s", serveHttpAddr) 43 | } 44 | -------------------------------------------------------------------------------- /httpserver/nocache.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Unix epoch time 9 | var epoch = time.Unix(0, 0).Format(time.RFC1123) 10 | 11 | // Taken from https://github.com/mytrile/nocache 12 | var noCacheHeaders = map[string]string{ 13 | "Expires": epoch, 14 | "Cache-Control": "no-cache, private, max-age=0", 15 | "Pragma": "no-cache", 16 | "X-Accel-Expires": "0", 17 | } 18 | 19 | var etagHeaders = []string{ 20 | "ETag", 21 | "If-Modified-Since", 22 | "If-Match", 23 | "If-None-Match", 24 | "If-Range", 25 | "If-Unmodified-Since", 26 | } 27 | 28 | // NoCache is a simple piece of middleware that sets a number of HTTP headers to prevent 29 | // a router (or subrouter) from being cached by an upstream proxy and/or client. 30 | // 31 | // As per http://wiki.nginx.org/HttpProxyModule - NoCache sets: 32 | // Expires: Thu, 01 Jan 1970 00:00:00 UTC 33 | // Cache-Control: no-cache, private, max-age=0 34 | // X-Accel-Expires: 0 35 | // Pragma: no-cache (for HTTP/1.0 proxies/clients) 36 | func NoCache(h http.Handler) http.Handler { 37 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | 39 | // Delete any ETag headers that may have been set 40 | for _, v := range etagHeaders { 41 | if r.Header.Get(v) != "" { 42 | r.Header.Del(v) 43 | } 44 | } 45 | 46 | // Set our NoCache headers 47 | for k, v := range noCacheHeaders { 48 | w.Header().Set(k, v) 49 | } 50 | 51 | h.ServeHTTP(w, r) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /inpipe/intcp.go: -------------------------------------------------------------------------------- 1 | package inpipe 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/covrom/hls-streamer/manifestgenerator" 10 | ) 11 | 12 | func InTCP(serveTCP string, readBufferSize int, mg *manifestgenerator.ManifestGenerator, log *logrus.Logger) { 13 | buf := make([]byte, 0, readBufferSize) 14 | listener, err := net.Listen("tcp", serveTCP) 15 | if err != nil { 16 | log.Fatal(err) 17 | os.Exit(1) 18 | } 19 | 20 | log.Printf("TCP server listening %s", serveTCP) 21 | 22 | chconn := make(chan net.Conn) 23 | 24 | go func() { 25 | for conn := range chconn { 26 | for { 27 | n, err := conn.Read(buf[:cap(buf)]) 28 | 29 | if n == 0 && err == io.EOF { 30 | log.Info("Closing process detected EOF") 31 | break 32 | } 33 | 34 | if err != nil && err != io.EOF { 35 | // Error reading pipe 36 | log.Fatal(err) 37 | os.Exit(1) 38 | } 39 | 40 | // process buf 41 | log.Debug("Sent to process: ", n, " bytes") 42 | mg.AddData(buf[:n]) 43 | } 44 | log.Debug("EOF source from: ", conn.RemoteAddr()) 45 | conn.Close() 46 | } 47 | }() 48 | 49 | for { 50 | log.Println("Wait for connecting source...") 51 | conn, err := listener.Accept() 52 | if err != nil { 53 | log.Printf("error accepting connection %v", err) 54 | continue 55 | } 56 | log.Debug("Connected source from: ", conn.RemoteAddr()) 57 | select { 58 | case chconn <- conn: 59 | default: 60 | log.Debug("Decline source from: ", conn.RemoteAddr()) 61 | conn.Close() 62 | } 63 | } 64 | 65 | // listener.Close() 66 | } 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 4 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 5 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 6 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= 7 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 11 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 12 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 17 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | -------------------------------------------------------------------------------- /tspacket/tspacket_test.go: -------------------------------------------------------------------------------- 1 | package tspacket 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | func parseHexString(h string) []byte { 9 | b, err := hex.DecodeString(h) 10 | if err != nil { 11 | panic("bad test: " + h) 12 | } 13 | return b 14 | } 15 | 16 | func TestTSPacketPIDNoIDR(t *testing.T) { 17 | tsPckt := New(TsDefaultPacketSize) 18 | 19 | // Generate TS packet 20 | buf := parseHexString("474011100042F0250001C10000FF01FF0001FC80144812010646466D70656709536572766963653031777C43CAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") 21 | tsPckt.AddData(buf) 22 | tsPckt.Parse(-1) 23 | 24 | videoPid := 17 25 | 26 | xpectedPid := videoPid 27 | if pid := tsPckt.GetPID(); pid != xpectedPid { 28 | t.Errorf("Pid is not correct, got = %d, want %d", pid, xpectedPid) 29 | } 30 | 31 | xpectedPCRS := -1.0 32 | if pcrS := tsPckt.GetPCRS(); pcrS != xpectedPCRS { 33 | t.Errorf("PCR is not correct, got = %f, want %f", pcrS, xpectedPCRS) 34 | } 35 | 36 | xpectedisRandomAccess := false 37 | if isRandomAccess := tsPckt.IsRandomAccess(videoPid); isRandomAccess != xpectedisRandomAccess { 38 | t.Errorf("RandomAccess is not correct, got = %t, want %t", isRandomAccess, xpectedisRandomAccess) 39 | } 40 | } 41 | 42 | func TestTSPacketPIDIDRPCR(t *testing.T) { 43 | tsPckt := New(TsDefaultPacketSize) 44 | 45 | // Generate TS packet 46 | buf := parseHexString("47410030075000007B0C7E00000001E0000080C00A310007EFD1110007D8610000000109F000000001674D4029965280A00B74A40404050000030001000003003C840000000168E90935200000000165888040006B6FFEF7D4B7CCB2D9A9BED82EA3DE8A78997D0DD494066F86757E1D7F4A3FA82C376EE9C0FE81F4F746A24E305C9A3E0DD5859DE0D287E8BEF70EA0CCF9008A25F52EF9A9CFA59B78AA5D34CB88001425FE7AB544EF7171FC56F27719F9C72D13FA7B0F5F3211A6") 47 | tsPckt.AddData(buf) 48 | tsPckt.Parse(-1) 49 | 50 | videoPid := 256 51 | 52 | xpectedPid := videoPid 53 | if pid := tsPckt.GetPID(); pid != xpectedPid { 54 | t.Errorf("Pid is not correct, got = %d, want %d", pid, xpectedPid) 55 | } 56 | 57 | xpectedPCRS := 0.7 58 | if pcrS := tsPckt.GetPCRS(); pcrS != xpectedPCRS { 59 | t.Errorf("IDR is not correct, got = %f, want %f", pcrS, xpectedPCRS) 60 | } 61 | 62 | xpectedisRandomAccess := true 63 | if isRandomAccess := tsPckt.IsRandomAccess(videoPid); isRandomAccess != xpectedisRandomAccess { 64 | t.Errorf("RandomAccess is not correct, got = %t, want %t", isRandomAccess, xpectedisRandomAccess) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/multi-rendition.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BASE_DIR="../results/multirendition" 4 | 5 | # Clean up 6 | echo "Restarting ${BASE_DIR} directory" 7 | rm -rf $BASE_DIR/* 8 | mkdir -p $BASE_DIR 9 | 10 | # Create master playlist (this should be created after 1st chunk is uploaded) 11 | echo "Creating master playlist manifest (playlist.m3u8)" 12 | echo "#EXTM3U" > $BASE_DIR/playlist.m3u8 13 | echo "#EXT-X-VERSION:3" >> $BASE_DIR/playlist.m3u8 14 | echo "#EXT-X-STREAM-INF:BANDWIDTH=996000,RESOLUTION=854x480" >> $BASE_DIR/playlist.m3u8 15 | echo "480p.m3u8" >> $BASE_DIR/playlist.m3u8 16 | echo "#EXT-X-STREAM-INF:BANDWIDTH=548000,RESOLUTION=640x360" >> $BASE_DIR/playlist.m3u8 17 | echo "360p.m3u8" >> $BASE_DIR/playlist.m3u8 18 | 19 | # Upload master playlist 20 | curl http://localhost:9094/results/playlist.m3u8 --upload-file $BASE_DIR/playlist.m3u8 21 | 22 | # Select font path based in OS 23 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 24 | FONT_PATH='/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf' 25 | elif [[ "$OSTYPE" == "darwin"* ]]; then 26 | FONT_PATH='/Library/Fonts/Arial.ttf' 27 | fi 28 | 29 | # Creates pipes 30 | mkfifo $BASE_DIR/fifo-480p 31 | mkfifo $BASE_DIR/fifo-360p 32 | 33 | # Creates consumers 34 | cat "$BASE_DIR/fifo-480p" | ../bin/manifest-generator -lf ../logs/segmenter480p.log -l 3 -d 2 -f 480p_ -cf 480p.m3u8 & 35 | PID_480p=$! 36 | echo "Started manifest-generator for 480p as PID $PID_480p" 37 | cat "$BASE_DIR/fifo-360p" | ../bin/manifest-generator -lf ../logs/segmenter360p.log -l 3 -d 2 -f 360p_ -cf 360p.m3u8 & 38 | PID_360p=$! 39 | echo "Started manifest-generator for 360p as PID $PID_360p" 40 | 41 | # Start test signal 42 | ffmpeg -hide_banner -y \ 43 | -f lavfi -re -i smptebars=duration=6000:size=320x200:rate=30 \ 44 | -f lavfi -i sine=frequency=1000:duration=6000:sample_rate=48000 -pix_fmt yuv420p \ 45 | -vf scale=854x480 \ 46 | -vf "drawtext=fontfile=$FONT_PATH: text=\'RENDITION 480p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\': x=0: y=0: fontsize=10: fontcolor=pink: box=1: boxcolor=0x00000099" \ 47 | -c:v libx264 -b:v 900k -g 60 -profile:v baseline -preset veryfast \ 48 | -c:a aac -b:a 48k \ 49 | -f mpegts "$BASE_DIR/fifo-480p" \ 50 | -vf scale=640x360 \ 51 | -vf "drawtext=fontfile=$FONT_PATH: text=\'RENDITION 360p - Local time %{localtime\: %Y\/%m\/%d %H.%M.%S} (%{n})\': x=0: y=0: fontsize=10: fontcolor=pink: box=1: boxcolor=0x00000099" \ 52 | -c:v libx264 -b:v 500k -g 60 -profile:v baseline -preset veryfast \ 53 | -c:a aac -b:a 48k \ 54 | -f mpegts "$BASE_DIR/fifo-360p" 55 | 56 | # Clean up: Stop processes 57 | # If the input stream stops the segmenter processes exists themselves 58 | # kill $PID_480p 59 | # kill $PID_360p 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/covrom/hls-streamer/hls" 9 | "github.com/covrom/hls-streamer/httpserver" 10 | "github.com/covrom/hls-streamer/inpipe" 11 | "github.com/covrom/hls-streamer/logger" 12 | "github.com/covrom/hls-streamer/manifestgenerator" 13 | "github.com/covrom/hls-streamer/mediachunk" 14 | ) 15 | 16 | const ( 17 | readBufferSize = 128 18 | ) 19 | 20 | var ( 21 | verbose = flag.Bool("v", false, "enable to get verbose logging") 22 | baseOutPath = flag.String("p", "./results", "Output path") 23 | chunkBaseFilename = flag.String("f", "chunk_", "Chunks base filename") 24 | chunkListFilename = flag.String("cf", "chunklist.m3u8", "Chunklist filename") 25 | targetSegmentDurS = flag.Float64("t", 4.0, "Target chunk duration in seconds") 26 | liveWindowSize = flag.Int("w", 3, "Live window size in chunks") 27 | lhlsAdvancedChunks = flag.Int("l", 0, "If > 0 activates LHLS, and it indicates the number of advanced chunks to create") 28 | manifestTypeInt = flag.Int("m", int(hls.LiveWindow), "Manifest to generate (0- Vod, 1- Live event, 2- Live sliding window") 29 | autoPID = flag.Bool("apids", true, "Enable auto PID detection, if true no need to pass vpid and apid") 30 | videoPID = flag.Int("vpid", -1, "Video PID to parse") 31 | audioPID = flag.Int("apid", -1, "Audio PID to parse") 32 | chunkInitType = flag.Int("i", int(manifestgenerator.ChunkInitStart), "Indicates where to put the init data PAT and PMT packets (0- No ini data, 1- Init segment, 2- At the begining of each chunk") 33 | destinationType = flag.Int("d", 1, "Indicates where the destination (0- No output, 1- File + flag indicator, 2- HTTP chunked transfer)") 34 | httpScheme = flag.String("protocol", "http", "HTTP Scheme (http, https)") 35 | httpHost = flag.String("host", "localhost:9094", "HTTP Host") 36 | serveTCP = flag.String("tcp", "localhost:9555", "Enable TCP server at this port instead of stdin input stream") 37 | serveHttp = flag.String("http", ":9099", "Enable http server at this port") 38 | ) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | var log = logger.ConfigureLogger(*verbose) 44 | 45 | log.Info(manifestgenerator.Version) 46 | log.Info("Started tssegmenter") 47 | 48 | if *autoPID == false && manifestgenerator.ChunkInitTypes(*chunkInitType) != manifestgenerator.ChunkNoIni { 49 | log.Error("Manual PID mode and Chunk No ini data are not compatible") 50 | os.Exit(1) 51 | } 52 | 53 | chunkOutputType := mediachunk.OutputTypes(*destinationType) 54 | hlsOutputType := hls.OutputTypes(*destinationType) 55 | 56 | // Creating output dir if does not exists 57 | if chunkOutputType == mediachunk.ChunkOutputModeFile || hlsOutputType == hls.HlsOutputModeFile { 58 | os.MkdirAll(*baseOutPath, 0744) 59 | } 60 | 61 | tr := http.DefaultTransport 62 | client := http.Client{ 63 | Transport: tr, 64 | Timeout: 0, 65 | } 66 | 67 | mg := manifestgenerator.New(log, 68 | chunkOutputType, 69 | hlsOutputType, 70 | *baseOutPath, 71 | *chunkBaseFilename, 72 | *chunkListFilename, 73 | *targetSegmentDurS, 74 | manifestgenerator.ChunkInitTypes(*chunkInitType), 75 | *autoPID, 76 | -1, 77 | -1, 78 | hls.ManifestTypes(*manifestTypeInt), 79 | *liveWindowSize, 80 | *lhlsAdvancedChunks, 81 | &client, 82 | *httpScheme, 83 | *httpHost, 84 | ) 85 | 86 | if *serveHttp != "" && *destinationType == 1 { 87 | httpserver.HTTPServer(*baseOutPath, *chunkListFilename, *serveHttp, log) 88 | } 89 | 90 | // Reader 91 | if *serveTCP == "" { 92 | inpipe.InPipe(readBufferSize, &mg, log) 93 | } else { 94 | inpipe.InTCP(*serveTCP, readBufferSize, &mg, log) 95 | } 96 | 97 | mg.Close() 98 | 99 | log.Info("Exit because detected EOF in the input pipe") 100 | 101 | os.Exit(0) 102 | } 103 | -------------------------------------------------------------------------------- /hls/hls.go: -------------------------------------------------------------------------------- 1 | package hls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "path" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Version Indicates the package version 18 | var Version = "1.0.0" 19 | 20 | // ManifestTypes indicates the manifest type 21 | type ManifestTypes int 22 | 23 | const ( 24 | // Vod Indicates VOD manifest 25 | Vod ManifestTypes = iota 26 | 27 | //LiveEvent Indicates a live manifest type event (always growing) 28 | LiveEvent 29 | 30 | //LiveWindow Indicates a live manifest type sliding window (fixed size) 31 | LiveWindow 32 | ) 33 | 34 | // OutputTypes indicates the manifest type 35 | type OutputTypes int 36 | 37 | const ( 38 | // HlsOutputModeNone No no write data 39 | HlsOutputModeNone OutputTypes = iota 40 | 41 | // HlsOutputModeFile Saves chunks to file 42 | HlsOutputModeFile 43 | 44 | // HlsOutputModeHTTP chunks to chunked streaming server 45 | HlsOutputModeHTTP 46 | ) 47 | 48 | // Chunk Chunk information 49 | type Chunk struct { 50 | IsGrowing bool 51 | FileName string 52 | DurationS float64 53 | IsDisco bool 54 | } 55 | 56 | // Hls Hls chunklist 57 | type Hls struct { 58 | log *logrus.Logger 59 | manifestType ManifestTypes 60 | version int 61 | isIndependentSegments bool 62 | targetDurS float64 63 | slidingWindowSize int 64 | mseq int64 65 | dseq int64 66 | chunks []Chunk 67 | chunklistFileName string 68 | initChunkDataFileName string 69 | outputType OutputTypes 70 | httpClient *http.Client 71 | httpScheme string 72 | httpHost string 73 | 74 | isClosed bool 75 | } 76 | 77 | // New Creates a hls chunklist manifest 78 | func New( 79 | log *logrus.Logger, 80 | ManifestType ManifestTypes, 81 | version int, 82 | isIndependentSegments bool, 83 | targetDurS float64, 84 | slidingWindowSize int, 85 | chunklistFileName string, 86 | initChunkDataFileName string, 87 | outputType OutputTypes, 88 | httpClient *http.Client, 89 | httpScheme string, 90 | httpHost string, 91 | ) Hls { 92 | h := Hls{ 93 | log, 94 | ManifestType, 95 | version, 96 | isIndependentSegments, 97 | targetDurS, 98 | slidingWindowSize, 99 | 0, 100 | 0, 101 | make([]Chunk, 0), 102 | chunklistFileName, 103 | initChunkDataFileName, 104 | outputType, 105 | httpClient, 106 | httpScheme, 107 | httpHost, 108 | false, 109 | } 110 | 111 | return h 112 | } 113 | 114 | // SetInitChunk Adds a chunk init infomation 115 | func (p *Hls) SetInitChunk(initChunkFileName string) { 116 | p.initChunkDataFileName = initChunkFileName 117 | } 118 | 119 | func (p *Hls) saveChunklist() error { 120 | ret := error(nil) 121 | 122 | hlsStrByte := []byte(p.String()) 123 | 124 | if p.outputType == HlsOutputModeFile { 125 | ret = p.saveManifestToFile(hlsStrByte) 126 | } else if p.outputType == HlsOutputModeHTTP { 127 | ret = p.saveManifestToHTTP(hlsStrByte) 128 | } 129 | 130 | return ret 131 | } 132 | 133 | // CloseManifest Adds a chunk init infomation 134 | func (p *Hls) CloseManifest(saveChunklist bool) error { 135 | ret := error(nil) 136 | 137 | p.isClosed = true 138 | 139 | if saveChunklist { 140 | ret = p.saveChunklist() 141 | } 142 | 143 | return ret 144 | } 145 | 146 | // SetHlsVersion Sets manifest version 147 | func (p *Hls) SetHlsVersion(version int) { 148 | p.version = version 149 | } 150 | 151 | func (p *Hls) saveManifestToFile(manifestByte []byte) error { 152 | if p.chunklistFileName != "" { 153 | err := ioutil.WriteFile(p.chunklistFileName, manifestByte, 0644) 154 | if err != nil { 155 | return err 156 | } 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (p *Hls) saveManifestToHTTP(manifestByte []byte) error { 163 | 164 | if p.chunklistFileName != "" { 165 | req := &http.Request{ 166 | Method: "POST", 167 | URL: &url.URL{ 168 | Scheme: p.httpScheme, 169 | Host: p.httpHost, 170 | Path: "/" + p.chunklistFileName, 171 | }, 172 | ProtoMajor: 1, 173 | ProtoMinor: 1, 174 | ContentLength: -1, 175 | Body: ioutil.NopCloser(bytes.NewReader(manifestByte)), 176 | Header: http.Header{}, 177 | } 178 | 179 | if strings.ToLower(path.Ext(p.chunklistFileName)) == ".m3u8" { 180 | req.Header.Set("Content-Type", "application/vnd.apple.mpegurl") 181 | } 182 | 183 | _, err := p.httpClient.Do(req) 184 | 185 | if err != nil { 186 | p.log.Error("Error uploading ", p.chunklistFileName, ". Error: ", err) 187 | } else { 188 | p.log.Debug("Upload of ", p.chunklistFileName, " complete") 189 | } 190 | } 191 | 192 | return nil 193 | } 194 | 195 | // AddChunk Adds a new chunk 196 | func (p *Hls) AddChunk(chunkData Chunk, saveChunklist bool) error { 197 | ret := error(nil) 198 | 199 | p.chunks = append(p.chunks, chunkData) 200 | 201 | if p.manifestType == LiveWindow && len(p.chunks) > p.slidingWindowSize { 202 | //Remove first 203 | if p.chunks[0].IsDisco { 204 | 205 | } 206 | p.chunks = p.chunks[1:] 207 | p.mseq++ 208 | } 209 | 210 | if saveChunklist { 211 | ret = p.saveChunklist() 212 | } 213 | 214 | return ret 215 | } 216 | 217 | // String write info to chunklist.m3u8 218 | func (p *Hls) String() string { 219 | var buffer bytes.Buffer 220 | 221 | buffer.WriteString("#EXTM3U\n") 222 | buffer.WriteString("#EXT-X-VERSION:" + strconv.Itoa(p.version) + "\n") 223 | buffer.WriteString("#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(p.mseq, 10) + "\n") 224 | buffer.WriteString("#EXT-X-DISCONTINUITY-SEQUENCE:" + strconv.FormatInt(p.dseq, 10) + "\n") 225 | 226 | if p.manifestType == Vod { 227 | buffer.WriteString("#EXT-X-PLAYLIST-TYPE:VOD\n") 228 | 229 | } else if p.manifestType == LiveEvent { 230 | buffer.WriteString("#EXT-X-PLAYLIST-TYPE:EVENT\n") 231 | } 232 | 233 | buffer.WriteString("#EXT-X-TARGETDURATION:" + fmt.Sprintf("%.0f", p.targetDurS) + "\n") 234 | 235 | if p.isIndependentSegments { 236 | buffer.WriteString("#EXT-X-INDEPENDENT-SEGMENTS\n") 237 | } 238 | 239 | if p.initChunkDataFileName != "" { 240 | chunkPath, _ := filepath.Rel(path.Dir(p.chunklistFileName), p.initChunkDataFileName) 241 | buffer.WriteString("#EXT-X-MAP:URI=\"" + chunkPath + "\"\n") 242 | } 243 | 244 | for _, chunk := range p.chunks { 245 | if chunk.IsDisco { 246 | buffer.WriteString("#EXT-X-DISCONTINUITY\n") 247 | } 248 | buffer.WriteString("#EXTINF:" + fmt.Sprintf("%.8f", chunk.DurationS) + ",\n") 249 | 250 | chunkPath, _ := filepath.Rel(path.Dir(p.chunklistFileName), chunk.FileName) 251 | buffer.WriteString(chunkPath + "\n") 252 | } 253 | 254 | if p.isClosed { 255 | buffer.WriteString("#EXT-X-ENDLIST\n") 256 | } 257 | 258 | return buffer.String() 259 | } 260 | -------------------------------------------------------------------------------- /mediachunk/mediachunk.go: -------------------------------------------------------------------------------- 1 | package mediachunk 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // OutputTypes indicates the manifest type 19 | type OutputTypes int 20 | 21 | const ( 22 | // ChunkOutputModeNone No no write data 23 | ChunkOutputModeNone OutputTypes = iota 24 | 25 | // ChunkOutputModeFile Saves chunks to file 26 | ChunkOutputModeFile 27 | 28 | // ChunkOutputModeHTTP chunks to chunked streaming server 29 | ChunkOutputModeHTTP 30 | ) 31 | 32 | // Options Chunking options 33 | type Options struct { 34 | Log *logrus.Logger 35 | OutputType OutputTypes 36 | LHLS bool 37 | EstimatedDurationS float64 38 | FileNumberLength int 39 | GhostPrefix string 40 | FileExtension string 41 | BasePath string 42 | ChunkBaseFilename string 43 | HTTPClient *http.Client 44 | HTTPScheme string 45 | HTTPHost string 46 | } 47 | 48 | // Chunk Chunk class 49 | type Chunk struct { 50 | fileWriter *bufio.Writer 51 | fileDescriptor *os.File 52 | 53 | httpWriteChan chan<- []byte 54 | httpReq *http.Request 55 | 56 | options Options 57 | 58 | index uint64 59 | filename string 60 | filenameGhost string 61 | 62 | totalBytes int 63 | } 64 | 65 | // New Creates a chunk instance 66 | func New(index uint64, options Options) Chunk { 67 | c := Chunk{nil, nil, nil, nil, options, index, "", "", 0} 68 | 69 | c.filename = c.createFilename(options.BasePath, options.ChunkBaseFilename, index, options.FileNumberLength, options.FileExtension, "") 70 | if options.GhostPrefix != "" { 71 | c.filenameGhost = c.createFilename(options.BasePath, options.ChunkBaseFilename, index, options.FileNumberLength, options.FileExtension, options.GhostPrefix) 72 | } 73 | 74 | return c 75 | } 76 | 77 | func (c *Chunk) initializeChunkFile() error { 78 | if c.filenameGhost != "" { 79 | // Create ghost file 80 | exists, _ := fileExists(c.filenameGhost) 81 | if !exists { 82 | err := ioutil.WriteFile(c.filenameGhost, nil, 0644) 83 | if err != nil { 84 | return err 85 | } 86 | } 87 | } 88 | 89 | if c.filename != "" { 90 | // Create ghost file 91 | exists, _ := fileExists(c.filename) 92 | if !exists { 93 | var err error 94 | c.fileDescriptor, err = os.Create(c.filename) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | c.fileWriter = bufio.NewWriter(c.fileDescriptor) 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (c *Chunk) initializeChunkHTTP() error { 107 | r, w := io.Pipe() 108 | writeChan := make(chan []byte) 109 | c.httpWriteChan = writeChan 110 | 111 | // open request 112 | req := &http.Request{ 113 | Method: "POST", 114 | URL: &url.URL{ 115 | Scheme: c.options.HTTPScheme, 116 | Host: c.options.HTTPHost, 117 | Path: "/" + c.filename, 118 | }, 119 | ProtoMajor: 1, 120 | ProtoMinor: 1, 121 | ContentLength: -1, 122 | Body: r, 123 | Header: http.Header{}, 124 | } 125 | 126 | if strings.ToLower(path.Ext(c.filename)) == ".ts" { 127 | req.Header.Set("Content-Type", "video/MP2T") 128 | } 129 | c.httpReq = req 130 | 131 | go func() { 132 | defer w.Close() 133 | 134 | for buf := range writeChan { 135 | n, err := w.Write(buf) 136 | c.options.Log.Debug("Wrote ", n, " bytes to chunk ", c.filename) 137 | if n != len(buf) && err != nil { 138 | panic(err) 139 | } 140 | } 141 | }() 142 | 143 | go func() { 144 | c.options.Log.Debug("Opening connection to upload ", c.filename) 145 | c.options.Log.Debug("Req: ", req) 146 | _, err := c.options.HTTPClient.Do(req) 147 | 148 | if err != nil { 149 | c.options.Log.Error("Error uploading ", c.filename, ". Error: ", err) 150 | } else { 151 | c.options.Log.Debug("Upload of ", c.filename, " complete") 152 | } 153 | }() 154 | 155 | return nil 156 | } 157 | 158 | //InitializeChunk Initializes chunk 159 | func (c *Chunk) InitializeChunk() error { 160 | ret := error(nil) 161 | 162 | if c.options.OutputType == ChunkOutputModeFile { 163 | ret = c.initializeChunkFile() 164 | } else if c.options.OutputType == ChunkOutputModeHTTP { 165 | ret = c.initializeChunkHTTP() 166 | } 167 | 168 | return ret 169 | } 170 | 171 | func (c *Chunk) closeChunkFile() { 172 | if c.filenameGhost != "" { 173 | exists, _ := fileExists(c.filenameGhost) 174 | if exists { 175 | os.Remove(c.filenameGhost) 176 | } 177 | } 178 | 179 | if c.fileWriter != nil { 180 | c.fileDescriptor.Close() 181 | } 182 | } 183 | 184 | func (c *Chunk) closeChunkHTTP() { 185 | if c.httpWriteChan != nil { 186 | close(c.httpWriteChan) 187 | } 188 | } 189 | 190 | //Close Closes chunk 191 | func (c *Chunk) Close() { 192 | c.options.Log.Debug("Closing chunk ", c.filename) 193 | if c.options.OutputType == ChunkOutputModeFile { 194 | c.closeChunkFile() 195 | } else if c.options.OutputType == ChunkOutputModeHTTP { 196 | c.closeChunkHTTP() 197 | } 198 | 199 | return 200 | } 201 | 202 | func (c *Chunk) addDataChunkFile(buf []byte) error { 203 | if c.fileWriter != nil { 204 | totalWrittenBytes := 0 205 | err := error(nil) 206 | 207 | for totalWrittenBytes < len(buf) && err == nil { 208 | writtenBytes, err := c.fileWriter.Write(buf[totalWrittenBytes:]) 209 | 210 | totalWrittenBytes = totalWrittenBytes + writtenBytes 211 | 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | c.fileWriter.Flush() 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func (c *Chunk) addDataChunkHTTP(buf []byte) error { 223 | if c.httpWriteChan != nil { 224 | bufCopy := make([]byte, len(buf)) 225 | copy(bufCopy, buf) 226 | 227 | c.httpWriteChan <- bufCopy 228 | } 229 | return nil 230 | } 231 | 232 | //AddData Add data to chunk and flush it 233 | func (c *Chunk) AddData(buf []byte) error { 234 | ret := error(nil) 235 | 236 | c.options.Log.Debug("Adding data to chunk ", c.filename) 237 | 238 | if c.options.OutputType == ChunkOutputModeFile { 239 | ret = c.addDataChunkFile(buf) 240 | } else if c.options.OutputType == ChunkOutputModeHTTP { 241 | ret = c.addDataChunkHTTP(buf) 242 | } 243 | 244 | c.totalBytes = c.totalBytes + len(buf) 245 | 246 | return ret 247 | } 248 | 249 | //IsEmpty Indicates if there are any bytes already saved in this chunk 250 | func (c *Chunk) IsEmpty() bool { 251 | ret := true 252 | if c.totalBytes > 0 { 253 | ret = false 254 | } 255 | 256 | return ret 257 | } 258 | 259 | //GetFilename Add data to chunk and flush it 260 | func (c *Chunk) GetFilename() string { 261 | return c.filename 262 | } 263 | 264 | func fileExists(filePath string) (bool, error) { 265 | _, err := os.Stat(filePath) 266 | if err == nil { 267 | return true, nil 268 | } 269 | if os.IsNotExist(err) { 270 | return false, nil 271 | } 272 | return true, err 273 | } 274 | 275 | func padNumberWithZero(value uint64, numZeros int) string { 276 | format := "%0" + strconv.Itoa(numZeros) + "d" 277 | return fmt.Sprintf(format, value) 278 | } 279 | 280 | func (c *Chunk) createFilename( 281 | basePath string, 282 | chunkBaseFilename string, 283 | index uint64, 284 | fileNumberLength int, 285 | fileExtension string, 286 | ghostPrefix string, 287 | ) string { 288 | ret := "" 289 | if ghostPrefix != "" { 290 | ret = path.Join(basePath, ghostPrefix+chunkBaseFilename+padNumberWithZero(index, fileNumberLength)+fileExtension) 291 | } else { 292 | ret = path.Join(basePath, chunkBaseFilename+padNumberWithZero(index, fileNumberLength)+fileExtension) 293 | } 294 | 295 | return ret 296 | } 297 | -------------------------------------------------------------------------------- /logs/segmenter.log: -------------------------------------------------------------------------------- 1 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T12:29:37+03:00"} 2 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T12:29:37+03:00"} 3 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T12:55:19+03:00"} 4 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T12:55:19+03:00"} 5 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T13:47:06+03:00"} 6 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T13:47:06+03:00"} 7 | {"level":"info","msg":"CHUNK! At PCRs: 4.721333333333333. ChunkDurS: 3.9999999999999996","time":"2019-10-18T13:47:18+03:00"} 8 | {"level":"info","msg":"CHUNK! At PCRs: 8.721333333333334. ChunkDurS: 4.000000000000001","time":"2019-10-18T13:47:22+03:00"} 9 | {"level":"info","msg":"CHUNK! At PCRs: 12.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:26+03:00"} 10 | {"level":"info","msg":"CHUNK! At PCRs: 16.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:30+03:00"} 11 | {"level":"info","msg":"CHUNK! At PCRs: 20.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:34+03:00"} 12 | {"level":"info","msg":"CHUNK! At PCRs: 24.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:38+03:00"} 13 | {"level":"info","msg":"CHUNK! At PCRs: 28.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:42+03:00"} 14 | {"level":"info","msg":"CHUNK! At PCRs: 32.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:46+03:00"} 15 | {"level":"info","msg":"CHUNK! At PCRs: 36.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:50+03:00"} 16 | {"level":"info","msg":"CHUNK! At PCRs: 40.721333333333334. ChunkDurS: 4","time":"2019-10-18T13:47:54+03:00"} 17 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T13:48:55+03:00"} 18 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T13:48:55+03:00"} 19 | {"level":"info","msg":"CHUNK! At PCRs: 8.356555555555556. ChunkDurS: 8.333333333333334","time":"2019-10-18T13:50:02+03:00"} 20 | {"level":"info","msg":"CHUNK! At PCRs: 16.689888888888888. ChunkDurS: 8.333333333333332","time":"2019-10-18T13:50:11+03:00"} 21 | {"level":"info","msg":"CHUNK! At PCRs: 25.023222222222223. ChunkDurS: 8.333333333333336","time":"2019-10-18T13:50:19+03:00"} 22 | {"level":"info","msg":"CHUNK! At PCRs: 33.35655555555556. ChunkDurS: 8.333333333333336","time":"2019-10-18T13:50:27+03:00"} 23 | {"level":"info","msg":"CHUNK! At PCRs: 41.68988888888889. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:50:36+03:00"} 24 | {"level":"info","msg":"CHUNK! At PCRs: 50.02322222222222. ChunkDurS: 8.333333333333336","time":"2019-10-18T13:50:44+03:00"} 25 | {"level":"info","msg":"CHUNK! At PCRs: 58.35655555555556. ChunkDurS: 8.333333333333336","time":"2019-10-18T13:50:52+03:00"} 26 | {"level":"info","msg":"CHUNK! At PCRs: 66.68988888888889. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:01+03:00"} 27 | {"level":"info","msg":"CHUNK! At PCRs: 75.02322222222222. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:09+03:00"} 28 | {"level":"info","msg":"CHUNK! At PCRs: 83.35655555555556. ChunkDurS: 8.333333333333343","time":"2019-10-18T13:51:17+03:00"} 29 | {"level":"info","msg":"CHUNK! At PCRs: 91.68988888888889. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:26+03:00"} 30 | {"level":"info","msg":"CHUNK! At PCRs: 100.02322222222222. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:34+03:00"} 31 | {"level":"info","msg":"CHUNK! At PCRs: 108.35655555555556. ChunkDurS: 8.333333333333343","time":"2019-10-18T13:51:42+03:00"} 32 | {"level":"info","msg":"CHUNK! At PCRs: 116.68988888888889. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:51+03:00"} 33 | {"level":"info","msg":"CHUNK! At PCRs: 125.02322222222222. ChunkDurS: 8.333333333333329","time":"2019-10-18T13:51:59+03:00"} 34 | {"level":"info","msg":"CHUNK! At PCRs: 133.35655555555556. ChunkDurS: 8.333333333333343","time":"2019-10-18T13:52:07+03:00"} 35 | {"level":"info","msg":"CHUNK! At PCRs: 141.6898888888889. ChunkDurS: 8.333333333333343","time":"2019-10-18T13:52:16+03:00"} 36 | {"level":"info","msg":"CHUNK! At PCRs: 150.02322222222222. ChunkDurS: 8.333333333333314","time":"2019-10-18T13:52:24+03:00"} 37 | {"level":"info","msg":"CHUNK! At PCRs: 158.35655555555556. ChunkDurS: 8.333333333333343","time":"2019-10-18T13:52:32+03:00"} 38 | {"level":"info","msg":"Closing process detected EOF","time":"2019-10-18T13:52:34+03:00"} 39 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T13:52:49+03:00"} 40 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T13:52:49+03:00"} 41 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T14:40:55+03:00"} 42 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T14:40:55+03:00"} 43 | {"level":"info","msg":"1.1.00xc000090c40","time":"2019-10-18T14:47:15+03:00"} 44 | {"level":"info","msg":"Started tssegmenter0xc000090c40","time":"2019-10-18T14:47:15+03:00"} 45 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-18T14:53:07+03:00"} 46 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-18T14:53:07+03:00"} 47 | {"level":"info","msg":"CHUNK! At PCRs: 8.333333333333334. ChunkDurS: 8.333333333333334","time":"2019-10-18T14:55:13+03:00"} 48 | {"level":"info","msg":"CHUNK! At PCRs: 12.633333333333333. ChunkDurS: 4.299999999999999","time":"2019-10-18T14:55:17+03:00"} 49 | {"level":"info","msg":"CHUNK! At PCRs: 20.5. ChunkDurS: 7.866666666666667","time":"2019-10-18T14:55:25+03:00"} 50 | {"level":"info","msg":"CHUNK! At PCRs: 28.833333333333332. ChunkDurS: 8.333333333333332","time":"2019-10-18T14:55:34+03:00"} 51 | {"level":"info","msg":"CHUNK! At PCRs: 37.166666666666664. ChunkDurS: 8.333333333333332","time":"2019-10-18T14:55:42+03:00"} 52 | {"level":"info","msg":"CHUNK! At PCRs: 45.5. ChunkDurS: 8.333333333333336","time":"2019-10-18T14:55:50+03:00"} 53 | {"level":"info","msg":"CHUNK! At PCRs: 53.833333333333336. ChunkDurS: 8.333333333333336","time":"2019-10-18T14:55:59+03:00"} 54 | {"level":"info","msg":"CHUNK! At PCRs: 62.166666666666664. ChunkDurS: 8.333333333333329","time":"2019-10-18T14:56:07+03:00"} 55 | {"level":"info","msg":"CHUNK! At PCRs: 70.5. ChunkDurS: 8.333333333333336","time":"2019-10-18T14:56:15+03:00"} 56 | {"level":"info","msg":"CHUNK! At PCRs: 78.83333333333333. ChunkDurS: 8.333333333333329","time":"2019-10-18T14:56:24+03:00"} 57 | {"level":"info","msg":"CHUNK! At PCRs: 87.16666666666667. ChunkDurS: 8.333333333333343","time":"2019-10-18T14:56:32+03:00"} 58 | {"level":"info","msg":"Closing process detected EOF","time":"2019-10-18T14:56:37+03:00"} 59 | {"level":"info","msg":"1.1.00xc000012c60","time":"2019-10-22T11:18:26+03:00"} 60 | {"level":"info","msg":"Started tssegmenter0xc000012c60","time":"2019-10-22T11:18:26+03:00"} 61 | {"level":"info","msg":"CHUNK! At PCRs: 8.333333333333334. ChunkDurS: 8.333333333333334","time":"2019-10-22T11:18:58+03:00"} 62 | {"level":"info","msg":"CHUNK! At PCRs: 12.966666666666667. ChunkDurS: 4.633333333333333","time":"2019-10-22T11:19:03+03:00"} 63 | {"level":"info","msg":"CHUNK! At PCRs: 21.3. ChunkDurS: 8.333333333333334","time":"2019-10-22T11:19:11+03:00"} 64 | {"level":"info","msg":"CHUNK! At PCRs: 29.633333333333333. ChunkDurS: 8.333333333333332","time":"2019-10-22T11:19:20+03:00"} 65 | {"level":"info","msg":"CHUNK! At PCRs: 37.96666666666667. ChunkDurS: 8.333333333333336","time":"2019-10-22T11:19:28+03:00"} 66 | {"level":"info","msg":"CHUNK! At PCRs: 46.3. ChunkDurS: 8.333333333333329","time":"2019-10-22T11:19:36+03:00"} 67 | {"level":"info","msg":"CHUNK! At PCRs: 54.63333333333333. ChunkDurS: 8.333333333333336","time":"2019-10-22T11:19:44+03:00"} 68 | {"level":"info","msg":"CHUNK! At PCRs: 62.96666666666667. ChunkDurS: 8.333333333333336","time":"2019-10-22T11:19:53+03:00"} 69 | {"level":"info","msg":"CHUNK! At PCRs: 71.3. ChunkDurS: 8.333333333333329","time":"2019-10-22T11:20:01+03:00"} 70 | {"level":"info","msg":"CHUNK! At PCRs: 79.63333333333334. ChunkDurS: 8.333333333333343","time":"2019-10-22T11:20:10+03:00"} 71 | {"level":"info","msg":"CHUNK! At PCRs: 87.96666666666667. ChunkDurS: 8.333333333333329","time":"2019-10-22T11:20:18+03:00"} 72 | {"level":"info","msg":"CHUNK! At PCRs: 96.3. ChunkDurS: 8.333333333333329","time":"2019-10-22T11:20:26+03:00"} 73 | {"level":"info","msg":"CHUNK! At PCRs: 103.73333333333333. ChunkDurS: 7.433333333333337","time":"2019-10-22T11:20:34+03:00"} 74 | {"level":"info","msg":"Closing process detected EOF","time":"2019-10-22T11:20:36+03:00"} 75 | -------------------------------------------------------------------------------- /tspacket/tspacket.go: -------------------------------------------------------------------------------- 1 | package tspacket 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | const ( 10 | // TsDefaultPacketSize Default TS packet size 11 | TsDefaultPacketSize int = 188 12 | 13 | // MaxPCRSValue (in seconds). 2^33 / 90000 (33 bits used by pcr with timebase of 90KHz) 14 | MaxPCRSValue float64 = 95443 15 | 16 | // tsStartByte Start byte for TS pakcets 17 | tsStartByte uint8 = 0x47 18 | 19 | // H264StreamType indicates h264 video ES 20 | H264StreamType uint8 = 0x1B 21 | 22 | // ADTSStreamType indicates audio ADTS ES 23 | ADTSStreamType uint8 = 0x0F 24 | 25 | // PATPID PID of PAT table 26 | PATPID uint16 = 0 27 | ) 28 | 29 | // transportPacketData TS packet info 30 | type transportPacketData struct { 31 | valid bool 32 | SyncByte uint8 33 | TransportErrorIndicator bool 34 | PayloadUnitStartIndicator bool 35 | TransportPriority bool 36 | PID uint16 37 | TransportScramblingControl uint8 38 | AdaptationFieldControl uint8 39 | ContinuityCounter uint8 40 | AdaptationField transportPacketAdaptationFieldData 41 | Pat programAddressTable 42 | Pmt programMapTable 43 | } 44 | 45 | // Reset transportPacketData 46 | func (t *transportPacketData) Reset() { 47 | t.valid = false 48 | t.SyncByte = 0 49 | t.TransportErrorIndicator = false 50 | t.PayloadUnitStartIndicator = false 51 | t.TransportErrorIndicator = false 52 | t.PID = 0 53 | t.TransportScramblingControl = 0 54 | t.AdaptationFieldControl = 0 55 | t.ContinuityCounter = 0 56 | t.AdaptationField.AdaptationFieldLength = 0 57 | t.AdaptationField.DiscontinuityIndicator = false 58 | t.AdaptationField.RandomAccessIndicator = false 59 | t.AdaptationField.ElementaryStreamPriorityIndicator = false 60 | t.AdaptationField.PCRFlag = false 61 | t.AdaptationField.OPCRFlag = false 62 | t.AdaptationField.SplicingPointFlag = false 63 | t.AdaptationField.TransportPrivateDataFlag = false 64 | t.AdaptationField.AdaptationFieldExtensionFlag = false 65 | t.AdaptationField.PCRData.valid = false 66 | t.AdaptationField.PCRData.ProgramClockReferenceBase = 0 67 | t.AdaptationField.PCRData.ProgramClockReferenceExtension = 0 68 | t.AdaptationField.PCRData.reserved = 0 69 | t.AdaptationField.PCRData.PCRs = 0 70 | t.Pat.valid = false 71 | t.Pat.PmtPID = 0 72 | t.Pmt.valid = false 73 | t.Pmt.AudioADTS = t.Pmt.AudioADTS[:0] 74 | t.Pmt.Videoh264 = t.Pmt.Videoh264[:0] 75 | t.Pmt.Other = t.Pmt.Other[:0] 76 | } 77 | 78 | // transportPacketAdaptationFieldData TS adaptation field packet info 79 | type transportPacketAdaptationFieldData struct { 80 | AdaptationFieldLength uint8 81 | DiscontinuityIndicator bool 82 | RandomAccessIndicator bool 83 | ElementaryStreamPriorityIndicator bool 84 | PCRFlag bool 85 | OPCRFlag bool 86 | SplicingPointFlag bool 87 | TransportPrivateDataFlag bool 88 | AdaptationFieldExtensionFlag bool 89 | PCRData transportPacketAdaptationPCRFieldData 90 | } 91 | 92 | // transportPacketAdaptationPCRFieldData TS PCR field packet info 93 | type transportPacketAdaptationPCRFieldData struct { 94 | ProgramClockReferenceBase uint64 95 | reserved uint8 96 | ProgramClockReferenceExtension uint16 97 | PCRs float64 98 | valid bool 99 | } 100 | 101 | // PAT data storing the PMT ID 102 | type programAddressTable struct { 103 | valid bool 104 | PmtPID uint16 105 | } 106 | 107 | // PMT data storing the video and audio PIDs to process 108 | type programMapTable struct { 109 | valid bool 110 | Videoh264 []uint16 111 | AudioADTS []uint16 112 | Other []uint16 113 | } 114 | 115 | // TsPacket Transport stream packet 116 | type TsPacket struct { 117 | buf []byte 118 | lastIndex int 119 | transportPacket transportPacketData 120 | pat programAddressTable 121 | pmt programMapTable 122 | } 123 | 124 | // New Creates a TsPacket instance 125 | func New(packetSize int) TsPacket { 126 | p := TsPacket{make([]byte, packetSize), 0, *new(transportPacketData), programAddressTable{valid: false, PmtPID: 0}, programMapTable{valid: false}} 127 | 128 | return p 129 | } 130 | 131 | // CloneFrom Deep clone all the packet 132 | func CloneFrom(srcPckt TsPacket) TsPacket { 133 | pcktSize := len(srcPckt.buf) 134 | 135 | newPckt := TsPacket{make([]byte, pcktSize), 0, *new(transportPacketData), programAddressTable{valid: false, PmtPID: 0}, programMapTable{valid: false}} 136 | copy(newPckt.buf, srcPckt.buf) 137 | 138 | // Copy all data 139 | newPckt.lastIndex = srcPckt.lastIndex 140 | newPckt.transportPacket = srcPckt.transportPacket 141 | newPckt.pat = srcPckt.pat 142 | 143 | newPckt.pmt.AudioADTS = make([]uint16, len(srcPckt.pmt.AudioADTS)) 144 | copy(newPckt.pmt.AudioADTS, srcPckt.pmt.AudioADTS) 145 | newPckt.pmt.Videoh264 = make([]uint16, len(srcPckt.pmt.Videoh264)) 146 | copy(newPckt.pmt.Videoh264, srcPckt.pmt.Videoh264) 147 | newPckt.pmt.Other = make([]uint16, len(srcPckt.pmt.Other)) 148 | copy(newPckt.pmt.Other, srcPckt.pmt.Other) 149 | newPckt.pmt.valid = srcPckt.pmt.valid 150 | 151 | return newPckt 152 | } 153 | 154 | // Reset packet 155 | func (p *TsPacket) Reset() { 156 | p.lastIndex = 0 157 | p.transportPacket.Reset() 158 | } 159 | 160 | // AddData Adds bytes to the packet 161 | func (p *TsPacket) AddData(buf []byte) { 162 | 163 | p.lastIndex = p.lastIndex + copy(p.buf[p.lastIndex:], buf[:]) 164 | } 165 | 166 | // GetBuffer Gets the buffer 167 | func (p *TsPacket) GetBuffer() []byte { 168 | return p.buf 169 | } 170 | 171 | // IsComplete Adds bytes to the packet 172 | func (p *TsPacket) IsComplete() bool { 173 | if p.lastIndex == TsDefaultPacketSize && p.buf[0] == tsStartByte { 174 | return true 175 | } 176 | return false 177 | } 178 | 179 | // Parse Parse the packet 180 | func (p *TsPacket) Parse(pmtPID int) bool { 181 | if !p.IsComplete() { 182 | return false 183 | } 184 | 185 | var transportPacket struct { 186 | SyncByte uint8 187 | ErrorIndicatorPayloadUnitPid uint16 188 | ScrambledAdapFieldContCounter uint8 189 | } 190 | 191 | r := bytes.NewReader(p.buf) 192 | err := binary.Read(r, binary.BigEndian, &transportPacket) 193 | if err != nil { 194 | return false 195 | } 196 | p.transportPacket.Reset() 197 | 198 | p.transportPacket.SyncByte = transportPacket.SyncByte 199 | if transportPacket.ErrorIndicatorPayloadUnitPid&0x8000 > 0 { 200 | p.transportPacket.TransportErrorIndicator = true 201 | } 202 | if transportPacket.ErrorIndicatorPayloadUnitPid&0x4000 > 0 { 203 | p.transportPacket.PayloadUnitStartIndicator = true 204 | } 205 | if transportPacket.ErrorIndicatorPayloadUnitPid&0x2000 > 0 { 206 | p.transportPacket.TransportPriority = true 207 | } 208 | p.transportPacket.PID = transportPacket.ErrorIndicatorPayloadUnitPid & 0x1FFF 209 | 210 | p.transportPacket.TransportScramblingControl = (transportPacket.ScrambledAdapFieldContCounter & 0xC0) >> 6 211 | p.transportPacket.AdaptationFieldControl = (transportPacket.ScrambledAdapFieldContCounter & 0x30) >> 4 212 | p.transportPacket.ContinuityCounter = transportPacket.ScrambledAdapFieldContCounter & 0x0F 213 | 214 | if p.transportPacket.AdaptationFieldControl == 2 || p.transportPacket.AdaptationFieldControl == 3 { 215 | var adaptationFieldLength uint8 216 | err := binary.Read(r, binary.BigEndian, &adaptationFieldLength) 217 | if err != nil { 218 | return false 219 | } 220 | 221 | if adaptationFieldLength > 0 { 222 | var adaptationFieldFlags uint8 223 | err := binary.Read(r, binary.BigEndian, &adaptationFieldFlags) 224 | if err != nil { 225 | return false 226 | } 227 | if (adaptationFieldFlags & 0x80) > 0 { 228 | p.transportPacket.AdaptationField.DiscontinuityIndicator = true 229 | } 230 | if (adaptationFieldFlags & 0x40) > 0 { 231 | p.transportPacket.AdaptationField.RandomAccessIndicator = true 232 | } 233 | if (adaptationFieldFlags & 0x20) > 0 { 234 | p.transportPacket.AdaptationField.ElementaryStreamPriorityIndicator = true 235 | } 236 | if (adaptationFieldFlags & 0x10) > 0 { 237 | p.transportPacket.AdaptationField.PCRFlag = true 238 | } 239 | if (adaptationFieldFlags & 0x08) > 0 { 240 | p.transportPacket.AdaptationField.OPCRFlag = true 241 | } 242 | if (adaptationFieldFlags & 0x04) > 0 { 243 | p.transportPacket.AdaptationField.SplicingPointFlag = true 244 | } 245 | if (adaptationFieldFlags & 0x02) > 0 { 246 | p.transportPacket.AdaptationField.TransportPrivateDataFlag = true 247 | } 248 | if (adaptationFieldFlags & 0x01) > 0 { 249 | p.transportPacket.AdaptationField.AdaptationFieldExtensionFlag = true 250 | } 251 | 252 | if p.transportPacket.AdaptationField.PCRFlag == true { 253 | var pcrDataFirst32b uint32 254 | err := binary.Read(r, binary.BigEndian, &pcrDataFirst32b) 255 | if err != nil { 256 | return false 257 | } 258 | 259 | var pcrDataLast16b uint16 260 | err = binary.Read(r, binary.BigEndian, &pcrDataLast16b) 261 | if err != nil { 262 | return false 263 | } 264 | p.transportPacket.AdaptationField.PCRData.ProgramClockReferenceExtension = uint16(pcrDataLast16b & 0x1FF) 265 | p.transportPacket.AdaptationField.PCRData.reserved = uint8((pcrDataLast16b >> 9) & 0x3F) 266 | 267 | p.transportPacket.AdaptationField.PCRData.ProgramClockReferenceBase = uint64(pcrDataFirst32b)*2 + uint64((pcrDataLast16b>>15)&0x1) 268 | 269 | p.transportPacket.AdaptationField.PCRData.PCRs = calculatePCRS(p.transportPacket.AdaptationField.PCRData.ProgramClockReferenceBase, p.transportPacket.AdaptationField.PCRData.ProgramClockReferenceExtension) 270 | 271 | p.transportPacket.AdaptationField.PCRData.valid = true 272 | } 273 | } 274 | } 275 | 276 | if p.transportPacket.PID == PATPID || int(p.transportPacket.PID) == pmtPID { 277 | if p.transportPacket.PayloadUnitStartIndicator { 278 | var length uint8 279 | 280 | err = binary.Read(r, binary.BigEndian, &length) 281 | if err != nil { 282 | return false 283 | } 284 | 285 | var n uint8 286 | var dummyByte uint8 287 | for n < length { 288 | err = binary.Read(r, binary.BigEndian, &dummyByte) 289 | if err != nil { 290 | return false 291 | } 292 | 293 | n++ 294 | } 295 | } 296 | } 297 | 298 | // PAT Packet (Getting the 1st PMT info, so assuming only one program and there is NO network ID) 299 | if p.transportPacket.PID == PATPID { 300 | var pmtPID16b struct { 301 | TableID uint8 302 | FlagsReservedSectionLength uint16 303 | TSId uint16 304 | Flags uint8 305 | SectionNumber uint8 306 | LastSectionNumber uint8 307 | 308 | //Initial PMT 309 | ProgramNumber uint16 310 | ReservedPMTPID uint16 311 | } 312 | err = binary.Read(r, binary.BigEndian, &pmtPID16b) 313 | if err != nil { 314 | return false 315 | } 316 | p.transportPacket.Pat.PmtPID = pmtPID16b.ReservedPMTPID & 0x1FFF 317 | p.transportPacket.Pat.valid = true 318 | } 319 | 320 | // PMT Packet 321 | if pmtPID == int(p.transportPacket.PID) { 322 | var tableInfo struct { 323 | _ uint8 324 | SectionLength uint16 325 | _ uint32 326 | _ uint16 327 | _ uint8 328 | ProgamInfoLength uint16 329 | } 330 | err = binary.Read(r, binary.BigEndian, &tableInfo) 331 | if err != nil { 332 | return false 333 | } 334 | 335 | sectionLength := tableInfo.SectionLength & 0x0FFF 336 | tableEnd := int(sectionLength - 13) 337 | 338 | programInfoLength := tableInfo.ProgamInfoLength & 0x0FFF 339 | 340 | paddingBytes := programInfoLength 341 | offset := 0 342 | 343 | for offset < tableEnd { 344 | for paddingBytes > 0 { 345 | var pad uint8 346 | err = binary.Read(r, binary.BigEndian, &pad) 347 | if err != nil { 348 | return false 349 | } 350 | paddingBytes = paddingBytes - 1 351 | offset = offset + 1 352 | } 353 | var program struct { 354 | StreamType uint8 355 | PID uint16 356 | Next uint16 357 | } 358 | err = binary.Read(r, binary.BigEndian, &program) 359 | if err != nil { 360 | return false 361 | } 362 | offset = offset + 5 363 | 364 | paddingBytes = program.Next & 0x0FFF 365 | pid := program.PID & 0x1FFF 366 | 367 | switch program.StreamType { 368 | case H264StreamType: 369 | p.transportPacket.Pmt.Videoh264 = append(p.transportPacket.Pmt.Videoh264, pid) 370 | case ADTSStreamType: 371 | p.transportPacket.Pmt.AudioADTS = append(p.transportPacket.Pmt.AudioADTS, pid) 372 | default: 373 | p.transportPacket.Pmt.Other = append(p.transportPacket.Pmt.Other, pid) 374 | } 375 | 376 | p.transportPacket.Pmt.valid = true 377 | } 378 | } 379 | 380 | p.transportPacket.valid = true 381 | 382 | return true 383 | } 384 | 385 | func calculatePCRS(pcrBase uint64, pcrExtension uint16) (PCRs float64) { 386 | PCRs = -1 387 | 388 | if pcrExtension > 0 { 389 | PCRs = float64(pcrBase*300.0+uint64(pcrExtension)) / (27.0 * 1000000.0) 390 | } else { 391 | PCRs = float64(pcrBase) / 90000.0 392 | } 393 | 394 | return 395 | } 396 | 397 | // GetPCRS Get PCT in seconds 398 | func (p *TsPacket) GetPCRS() (PCRs float64) { 399 | PCRs = -1 400 | if !p.transportPacket.valid || !p.transportPacket.AdaptationField.PCRData.valid { 401 | return 402 | } 403 | 404 | PCRs = p.transportPacket.AdaptationField.PCRData.PCRs 405 | 406 | return 407 | } 408 | 409 | // GetPATdata Gets the PAT info if present (so PMT PID) 410 | func (p *TsPacket) GetPATdata() (PMTPID int) { 411 | PMTPID = -1 412 | if !p.transportPacket.valid || !p.transportPacket.Pat.valid { 413 | return 414 | } 415 | 416 | PMTPID = int(p.transportPacket.Pat.PmtPID) 417 | 418 | return 419 | } 420 | 421 | // GetPMTdata Gets the PMT dta if present (video, audios, and other PIDs) 422 | func (p *TsPacket) GetPMTdata() (valid bool, Videoh264 []uint16, AudioADTS []uint16, Other []uint16) { 423 | valid = false 424 | if !p.transportPacket.valid || !p.transportPacket.Pmt.valid { 425 | return 426 | } 427 | 428 | Videoh264 = p.transportPacket.Pmt.Videoh264 429 | AudioADTS = p.transportPacket.Pmt.AudioADTS 430 | Other = p.transportPacket.Pmt.Other 431 | valid = true 432 | 433 | return 434 | } 435 | 436 | // GetPID Adds bytes to the packet 437 | func (p *TsPacket) GetPID() (pID int) { 438 | pID = -1 439 | if !p.transportPacket.valid { 440 | return 441 | } 442 | 443 | pID = int(p.transportPacket.PID) 444 | 445 | return 446 | } 447 | 448 | // String retuns packet data in string form 449 | func (p *TsPacket) String() string { 450 | ret := "" 451 | if !p.transportPacket.valid { 452 | return ret 453 | } 454 | 455 | ret = fmt.Sprintf("%+v", p.transportPacket) 456 | return ret 457 | } 458 | 459 | // IsRandomAccess Return true if this is a random access point 460 | func (p *TsPacket) IsRandomAccess(pID int) (isIDR bool) { 461 | isIDR = false 462 | if !p.transportPacket.valid { 463 | return 464 | } 465 | 466 | if p.transportPacket.PID == uint16(pID) { 467 | if p.transportPacket.AdaptationFieldControl == 2 || p.transportPacket.AdaptationFieldControl == 3 { 468 | if p.transportPacket.AdaptationField.RandomAccessIndicator == true { 469 | isIDR = true 470 | } 471 | } 472 | } 473 | 474 | return 475 | } 476 | -------------------------------------------------------------------------------- /manifestgenerator/manifestgenerator_test.go: -------------------------------------------------------------------------------- 1 | package manifestgenerator 2 | 3 | import ( 4 | "bufio" 5 | "encoding/hex" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "testing" 11 | 12 | "github.com/covrom/hls-streamer/hls" 13 | "github.com/covrom/hls-streamer/mediachunk" 14 | ) 15 | 16 | func parseHexString(h string) []byte { 17 | b, err := hex.DecodeString(h) 18 | if err != nil { 19 | panic("bad test: " + h) 20 | } 21 | return b 22 | } 23 | 24 | // Clear directory results 25 | func clearResultsDir(pathResults string) { 26 | os.RemoveAll(pathResults) 27 | 28 | os.MkdirAll(pathResults, 0744) 29 | } 30 | 31 | func TestManifestGeneratorBasic1Pckt(t *testing.T) { 32 | pathResults := "../results/Basic1Pckt" 33 | clearResultsDir(pathResults) 34 | 35 | mg := New(nil, mediachunk.ChunkOutputModeNone, hls.HlsOutputModeNone, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkNoIni, false, 256, 257, hls.LiveWindow, 3, 0, nil, "", "") 36 | 37 | // Generate TS packet 38 | pckt := parseHexString("47410030075000007B0C7E00000001E0000080C00A310007EFD1110007D8610000000109F000000001674D4029965280A00B74A40404050000030001000003003C840000000168E90935200000000165888040006B6FFEF7D4B7CCB2D9A9BED82EA3DE8A78997D0DD494066F86757E1D7F4A3FA82C376EE9C0FE81F4F746A24E305C9A3E0DD5859DE0D287E8BEF70EA0CCF9008A25F52EF9A9CFA59B78AA5D34CB88001425FE7AB544EF7171FC56F27719F9C72D13FA7B0F5F3211A6") 39 | 40 | mg.AddData(pckt) 41 | 42 | //mg.Close() 43 | 44 | xpectednumProcPackets := uint64(1) 45 | procPckts := mg.getNumProcessedPackets() 46 | if procPckts != xpectednumProcPackets { 47 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 48 | } 49 | } 50 | 51 | func TestManifestGeneratorBasic2Pckt(t *testing.T) { 52 | pathResults := "../results/Basic2Pckt" 53 | clearResultsDir(pathResults) 54 | 55 | mg := New(nil, mediachunk.ChunkOutputModeNone, hls.HlsOutputModeNone, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkNoIni, false, 256, 257, hls.LiveWindow, 3, 0, nil, "", "") 56 | 57 | // Generate TS packet 58 | pckt := parseHexString( 59 | "47410030075000007B0C7E00000001E0000080C00A310007EFD1110007D8610000000109F000000001674D4029965280A00B74A40404050000030001000003003C840000000168E90935200000000165888040006B6FFEF7D4B7CCB2D9A9BED82EA3DE8A78997D0DD494066F86757E1D7F4A3FA82C376EE9C0FE81F4F746A24E305C9A3E0DD5859DE0D287E8BEF70EA0CCF9008A25F52EF9A9CFA59B78AA5D34CB88001425FE7AB544EF7171FC56F27719F9C72D13FA7B0F5F3211A6" + 60 | "47410030075000007B0C7E00000001E0000080C00A310007EFD1110007D8610000000109F000000001674D4029965280A00B74A40404050000030001000003003C840000000168E90935200000000165888040006B6FFEF7D4B7CCB2D9A9BED82EA3DE8A78997D0DD494066F86757E1D7F4A3FA82C376EE9C0FE81F4F746A24E305C9A3E0DD5859DE0D287E8BEF70EA0CCF9008A25F52EF9A9CFA59B78AA5D34CB88001425FE7AB544EF7171FC56F27719F9C72D13FA7B0F5F3211A6") 61 | 62 | mg.AddData(pckt) 63 | 64 | mg.Close() 65 | 66 | xpectednumProcPackets := uint64(2) 67 | procPckts := mg.getNumProcessedPackets() 68 | if procPckts != xpectednumProcPackets { 69 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 70 | } 71 | } 72 | 73 | func TestManifestGeneratorBasicVideoBigPacketsNoIniData(t *testing.T) { 74 | pathResults := "../results/VideoBigPackets" 75 | clearResultsDir(pathResults) 76 | 77 | f, err := os.Open("../fixture/testSmall.ts") 78 | if err != nil { 79 | panic("Error opening test file") 80 | } 81 | 82 | mediaSourceReader := bufio.NewReader(f) 83 | buf := make([]byte, 0, 4*1024) //4KB Buffers 84 | 85 | mg := New(nil, mediachunk.ChunkOutputModeNone, hls.HlsOutputModeNone, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkNoIni, false, 256, 257, hls.LiveWindow, 3, 0, nil, "", "") 86 | 87 | for { 88 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 89 | buf = buf[:n] 90 | if n == 0 { 91 | if err == nil { 92 | continue 93 | } 94 | if err == io.EOF { 95 | break 96 | } 97 | } else { 98 | mg.AddData(buf) 99 | } 100 | // process buf 101 | if err != nil && err != io.EOF { 102 | panic("Error reading test file") 103 | } 104 | } 105 | mg.Close() 106 | 107 | xpectednumProcPackets := uint64(1835) 108 | procPckts := mg.getNumProcessedPackets() 109 | if procPckts != xpectednumProcPackets { 110 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 111 | } 112 | } 113 | 114 | func TestManifestGeneratorBasicVideoSmallPacketsNoIniData(t *testing.T) { 115 | pathResults := "../results/VideoSmallPackets" 116 | clearResultsDir(pathResults) 117 | 118 | f, err := os.Open("../fixture/testSmall.ts") 119 | if err != nil { 120 | panic("Error opening test file") 121 | } 122 | 123 | mediaSourceReader := bufio.NewReader(f) 124 | buf := make([]byte, 0, 100) //100 bytes 125 | 126 | mg := New(nil, mediachunk.ChunkOutputModeNone, hls.HlsOutputModeNone, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkNoIni, false, 256, 257, hls.LiveWindow, 3, 0, nil, "", "") 127 | 128 | for { 129 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 130 | buf = buf[:n] 131 | if n == 0 { 132 | if err == nil { 133 | continue 134 | } 135 | if err == io.EOF { 136 | break 137 | } 138 | } else { 139 | mg.AddData(buf) 140 | } 141 | // process buf 142 | if err != nil && err != io.EOF { 143 | panic("Error reading test file") 144 | } 145 | } 146 | mg.Close() 147 | 148 | xpectednumProcPackets := uint64(1835) 149 | procPckts := mg.getNumProcessedPackets() 150 | if procPckts != xpectednumProcPackets { 151 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 152 | } 153 | } 154 | 155 | func TestManifestGeneratorInitialResyncVideoBigPacketsNoIniData(t *testing.T) { 156 | pathResults := "../results/VideoResyncBigPackets" 157 | clearResultsDir(pathResults) 158 | 159 | f, err := os.Open("../fixture/testSmall.ts") 160 | if err != nil { 161 | panic("Error opening test file") 162 | } 163 | 164 | mediaSourceReader := bufio.NewReader(f) 165 | buf := make([]byte, 0, 4*1024) //4KB Buffers 166 | 167 | mg := New(nil, mediachunk.ChunkOutputModeNone, hls.HlsOutputModeNone, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkNoIni, false, 256, 257, hls.LiveWindow, 3, 0, nil, "", "") 168 | 169 | // Start out of sync 170 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 171 | if err != nil { 172 | panic("Error reading test file") 173 | } 174 | 175 | for { 176 | n, err = mediaSourceReader.Read(buf[:cap(buf)]) 177 | buf = buf[:n] 178 | if n == 0 { 179 | if err == nil { 180 | continue 181 | } 182 | if err == io.EOF { 183 | break 184 | } 185 | } else { 186 | mg.AddData(buf) 187 | } 188 | // process buf 189 | if err != nil && err != io.EOF { 190 | panic("Error reading test file") 191 | } 192 | } 193 | mg.Close() 194 | 195 | xpectednumProcPackets := uint64(1813) 196 | procPckts := mg.getNumProcessedPackets() 197 | if procPckts != xpectednumProcPackets { 198 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 199 | } 200 | } 201 | 202 | func TestManifestGeneratorBasicVideoBigPacketsAutoPIDsInitSegment(t *testing.T) { 203 | pathResults := "../results/VideoBigPacketsAutoPIDsInitSegment" 204 | clearResultsDir(pathResults) 205 | 206 | f, err := os.Open("../fixture/testSmall.ts") 207 | if err != nil { 208 | panic("Error opening test file") 209 | } 210 | 211 | mediaSourceReader := bufio.NewReader(f) 212 | buf := make([]byte, 0, 4*1024) //4KB Buffers 213 | 214 | mg := New(nil, mediachunk.ChunkOutputModeFile, hls.HlsOutputModeFile, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkInit, true, -1, -1, hls.Vod, 3, 0, nil, "", "") 215 | 216 | for { 217 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 218 | buf = buf[:n] 219 | if n == 0 { 220 | if err == nil { 221 | continue 222 | } 223 | if err == io.EOF { 224 | break 225 | } 226 | } else { 227 | mg.AddData(buf) 228 | } 229 | // process buf 230 | if err != nil && err != io.EOF { 231 | panic("Error reading test file") 232 | } 233 | } 234 | mg.Close() 235 | 236 | xpectednumProcPackets := uint64(1835) 237 | procPckts := mg.getNumProcessedPackets() 238 | if procPckts != xpectednumProcPackets { 239 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 240 | } 241 | 242 | // Check chunks 243 | type fileData struct { 244 | name string 245 | size int64 246 | } 247 | 248 | filesData := []fileData{ 249 | { 250 | name: path.Join(pathResults, "init00000.ts"), 251 | size: 376, 252 | }, 253 | { 254 | name: path.Join(pathResults, "chunk_00000.ts"), 255 | size: 103024, 256 | }, 257 | { 258 | name: path.Join(pathResults, "chunk_00001.ts"), 259 | size: 108288, 260 | }, 261 | { 262 | name: path.Join(pathResults, "chunk_00002.ts"), 263 | size: 114680, 264 | }, 265 | } 266 | 267 | for _, filesToCheck := range filesData { 268 | fi, err := os.Stat(filesToCheck.name) 269 | if err != nil || fi.Size() != filesToCheck.size { 270 | t.Errorf("Error checking file %s, got %d bytes, expected %d bytes. Err: %v", filesToCheck.name, fi.Size(), filesToCheck.size, err) 271 | } 272 | } 273 | } 274 | 275 | func TestManifestGeneratorBasicVideoBigPacketsAutoPIDsInitStartSegment(t *testing.T) { 276 | pathResults := "../results/VideoBigPacketsAutoPIDsInitStartSegment" 277 | chunklistFile := "chunklist.m3u8" 278 | clearResultsDir(pathResults) 279 | 280 | f, err := os.Open("../fixture/testSmall.ts") 281 | if err != nil { 282 | panic("Error opening test file") 283 | } 284 | 285 | mediaSourceReader := bufio.NewReader(f) 286 | buf := make([]byte, 0, 4*1024) //4KB Buffers 287 | 288 | mg := New(nil, mediachunk.ChunkOutputModeFile, hls.HlsOutputModeFile, pathResults, "chunk_", "chunklist.m3u8", 4.0, ChunkInitStart, true, -1, -1, hls.Vod, 3, 0, nil, "", "") 289 | 290 | for { 291 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 292 | buf = buf[:n] 293 | if n == 0 { 294 | if err == nil { 295 | continue 296 | } 297 | if err == io.EOF { 298 | break 299 | } 300 | } else { 301 | mg.AddData(buf) 302 | } 303 | // process buf 304 | if err != nil && err != io.EOF { 305 | panic("Error reading test file") 306 | } 307 | } 308 | mg.Close() 309 | 310 | xpectednumProcPackets := uint64(1835) 311 | procPckts := mg.getNumProcessedPackets() 312 | if procPckts != xpectednumProcPackets { 313 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 314 | } 315 | 316 | // Check chunks 317 | type fileData struct { 318 | name string 319 | size int64 320 | } 321 | 322 | filesData := []fileData{ 323 | { 324 | name: path.Join(pathResults, "chunk_00000.ts"), 325 | size: 103400, 326 | }, 327 | { 328 | name: path.Join(pathResults, "chunk_00001.ts"), 329 | size: 108664, 330 | }, 331 | { 332 | name: path.Join(pathResults, "chunk_00002.ts"), 333 | size: 115056, 334 | }, 335 | } 336 | 337 | for _, filesToCheck := range filesData { 338 | fi, err := os.Stat(filesToCheck.name) 339 | if err != nil || fi.Size() != filesToCheck.size { 340 | t.Errorf("Error checking file %s, got %d bytes, expected %d bytes. Err: %v", filesToCheck.name, fi.Size(), filesToCheck.size, err) 341 | } 342 | } 343 | 344 | // Check HLS chunklist 345 | fileChunklistHLS, err := os.Open(path.Join(pathResults, chunklistFile)) 346 | if err != nil { 347 | t.Errorf("Error opening HLS chunklist!, Err: %v", err) 348 | } 349 | defer fileChunklistHLS.Close() 350 | 351 | manifestByte, err := ioutil.ReadAll(fileChunklistHLS) 352 | if err != nil { 353 | t.Errorf("Error reading HLS chunklist data!, Err: %v", err) 354 | } 355 | 356 | manifestStr := string(manifestByte) 357 | xpectedmanifestStr := `#EXTM3U 358 | #EXT-X-VERSION:3 359 | #EXT-X-MEDIA-SEQUENCE:0 360 | #EXT-X-DISCONTINUITY-SEQUENCE:0 361 | #EXT-X-PLAYLIST-TYPE:VOD 362 | #EXT-X-TARGETDURATION:4 363 | #EXT-X-INDEPENDENT-SEGMENTS 364 | #EXTINF:4.00000000, 365 | chunk_00000.ts 366 | #EXTINF:4.00000000, 367 | chunk_00001.ts 368 | #EXTINF:2.00000000, 369 | chunk_00002.ts 370 | #EXT-X-ENDLIST 371 | ` 372 | if manifestStr != xpectedmanifestStr { 373 | t.Errorf("Manifest data is different, got %s , expected %s", manifestStr, xpectedmanifestStr) 374 | } 375 | } 376 | 377 | func TestManifestGeneratorBasicVideoBigPacketsAutoPIDsInitStartSegmentLHLS(t *testing.T) { 378 | //TODO: Very simple test, we need more controls 379 | pathResults := "../results/VideoBigPacketsAutoPIDsInitStartSegmentLHLS" 380 | chunklistFile := "chunklist.m3u8" 381 | clearResultsDir(pathResults) 382 | 383 | f, err := os.Open("../fixture/testSmall.ts") 384 | if err != nil { 385 | panic("Error opening test file") 386 | } 387 | 388 | mediaSourceReader := bufio.NewReader(f) 389 | buf := make([]byte, 0, 4*1024) //4KB Buffers 390 | 391 | mg := New(nil, mediachunk.ChunkOutputModeFile, hls.HlsOutputModeFile, pathResults, "chunk_", chunklistFile, 4.0, ChunkInitStart, true, -1, -1, hls.LiveWindow, 3, 3, nil, "", "") 392 | 393 | for { 394 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 395 | buf = buf[:n] 396 | if n == 0 { 397 | if err == nil { 398 | continue 399 | } 400 | if err == io.EOF { 401 | break 402 | } 403 | } else { 404 | mg.AddData(buf) 405 | } 406 | // process buf 407 | if err != nil && err != io.EOF { 408 | panic("Error reading test file") 409 | } 410 | } 411 | mg.Close() 412 | 413 | xpectednumProcPackets := uint64(1835) 414 | procPckts := mg.getNumProcessedPackets() 415 | if procPckts != xpectednumProcPackets { 416 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 417 | } 418 | 419 | // Check chunks 420 | type fileData struct { 421 | name string 422 | size int64 423 | } 424 | 425 | filesData := []fileData{ 426 | { 427 | name: path.Join(pathResults, "chunk_00000.ts"), 428 | size: 103400, 429 | }, 430 | { 431 | name: path.Join(pathResults, "chunk_00001.ts"), 432 | size: 108664, 433 | }, 434 | { 435 | name: path.Join(pathResults, "chunk_00002.ts"), 436 | size: 115056, 437 | }, 438 | { 439 | name: path.Join(pathResults, "chunk_00003.ts"), 440 | size: 0, 441 | }, 442 | { 443 | name: path.Join(pathResults, "chunk_00004.ts"), 444 | size: 0, 445 | }, 446 | { 447 | name: path.Join(pathResults, ".growing_chunk_00003.ts"), 448 | size: 0, 449 | }, 450 | { 451 | name: path.Join(pathResults, ".growing_chunk_00004.ts"), 452 | size: 0, 453 | }, 454 | } 455 | 456 | for _, filesToCheck := range filesData { 457 | fi, err := os.Stat(filesToCheck.name) 458 | if err != nil || fi.Size() != filesToCheck.size { 459 | t.Errorf("Error checking file %s, got %d bytes, expected %d bytes. Err: %v", filesToCheck.name, fi.Size(), filesToCheck.size, err) 460 | } 461 | } 462 | 463 | // Check HLS chunklist 464 | fileChunklistHLS, err := os.Open(path.Join(pathResults, chunklistFile)) 465 | if err != nil { 466 | t.Errorf("Error opening HLS chunklist!, Err: %v", err) 467 | } 468 | defer fileChunklistHLS.Close() 469 | 470 | manifestByte, err := ioutil.ReadAll(fileChunklistHLS) 471 | if err != nil { 472 | t.Errorf("Error reading HLS chunklist data!, Err: %v", err) 473 | } 474 | 475 | manifestStr := string(manifestByte) 476 | xpectedmanifestStr := `#EXTM3U 477 | #EXT-X-VERSION:3 478 | #EXT-X-MEDIA-SEQUENCE:0 479 | #EXT-X-DISCONTINUITY-SEQUENCE:0 480 | #EXT-X-TARGETDURATION:4 481 | #EXT-X-INDEPENDENT-SEGMENTS 482 | #EXTINF:4.00000000, 483 | chunk_00000.ts 484 | #EXTINF:4.00000000, 485 | chunk_00001.ts 486 | #EXTINF:4.00000000, 487 | chunk_00002.ts 488 | #EXTINF:4.00000000, 489 | chunk_00003.ts 490 | #EXTINF:4.00000000, 491 | chunk_00004.ts 492 | ` 493 | if manifestStr != xpectedmanifestStr { 494 | t.Errorf("Manifest data is different, got %s , expected %s", manifestStr, xpectedmanifestStr) 495 | } 496 | } 497 | 498 | /* 499 | //TODO: Added HTTP test needs more work 500 | func TestManifestGeneratorBasicVideoBigPacketsAutoPIDsInitStartSegmentToHTTP(t *testing.T) { 501 | pathResults := "../results/VideoBigPacketsAutoPIDsInitStartSegmentToHTTP" 502 | chunklistFile := "chunklist.m3u8" 503 | clearResultsDir(pathResults) 504 | 505 | f, err := os.Open("../fixture/testSmall.ts") 506 | if err != nil { 507 | panic("Error opening test file") 508 | } 509 | 510 | mediaSourceReader := bufio.NewReader(f) 511 | buf := make([]byte, 0, 4*1024) //4KB Buffers 512 | 513 | tr := http.DefaultTransport 514 | client := http.Client{ 515 | Transport: tr, 516 | Timeout: 0, 517 | } 518 | 519 | mg := New(nil, mediachunk.ChunkOutputModeHTTP, hls.HlsOutputModeHTTP, pathResults, "chunk_", chunklistFile, 4.0, ChunkInitStart, true, -1, -1, hls.LiveWindow, 3, 0, &client, "http", "localhost:9094") 520 | 521 | for { 522 | n, err := mediaSourceReader.Read(buf[:cap(buf)]) 523 | buf = buf[:n] 524 | if n == 0 { 525 | if err == nil { 526 | continue 527 | } 528 | if err == io.EOF { 529 | break 530 | } 531 | } else { 532 | mg.AddData(buf) 533 | } 534 | // process buf 535 | if err != nil && err != io.EOF { 536 | panic("Error reading test file") 537 | } 538 | } 539 | mg.Close() 540 | 541 | xpectednumProcPackets := uint64(1835) 542 | procPckts := mg.getNumProcessedPackets() 543 | if procPckts != xpectednumProcPackets { 544 | t.Errorf("Processed packet number is incorrect, got: %d, want: %d.", procPckts, xpectednumProcPackets) 545 | } 546 | }*/ 547 | -------------------------------------------------------------------------------- /manifestgenerator/manifestgenerator.go: -------------------------------------------------------------------------------- 1 | package manifestgenerator 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "path" 7 | 8 | "github.com/covrom/hls-streamer/hls" 9 | "github.com/covrom/hls-streamer/mediachunk" 10 | "github.com/covrom/hls-streamer/tspacket" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Version Indicates the package version 15 | var Version = "1.1.0" 16 | 17 | // HlsDefaultVersion to use 18 | const HlsDefaultVersion int = 3 19 | 20 | // ChunkInitTypes types indicates where to put the init data (PAT and PMT) 21 | type ChunkInitTypes int 22 | 23 | const ( 24 | // ChunkNoIni Necessary if you choose manual PIDs selection 25 | ChunkNoIni ChunkInitTypes = iota 26 | 27 | //ChunkInit Creates the init segment 28 | ChunkInit 29 | 30 | //ChunkInitStart Adds PAT and PAT at each chunk start (CC will be broken) 31 | ChunkInitStart 32 | ) 33 | 34 | const ( 35 | //GhostPrefixDefault ghost chunk prefix 36 | GhostPrefixDefault = ".growing_" 37 | 38 | //ChunkFileNumberLength chunk filenumber length 39 | ChunkFileNumberLength = 5 40 | 41 | //ChunkFileExtensionDefault default chunk extension 42 | ChunkFileExtensionDefault = ".ts" 43 | 44 | //ChunkInitFileName Init chunk filename 45 | ChunkInitFileName = "init" 46 | ) 47 | 48 | const ( 49 | // ChunkLengthToleranceS Tolerance calculationg chunk length 50 | ChunkLengthToleranceS = 0.25 51 | ) 52 | 53 | // packetTableTypes 54 | type packetTableTypes int 55 | 56 | const ( 57 | // PatTable PAT 58 | PatTable = iota 59 | 60 | // PmtTable PAT 61 | PmtTable 62 | ) 63 | 64 | // initState 65 | type initStates int 66 | 67 | const ( 68 | // InitNotIni no PAT / PMT saved 69 | InitNotIni = iota 70 | 71 | // InitsavedPAT PAT saved, needs PMT too 72 | InitsavedPAT 73 | 74 | // InitsavedPMT PMT and PAT saved 75 | InitsavedPMT 76 | ) 77 | 78 | type options struct { 79 | log *logrus.Logger 80 | chunkOutputType mediachunk.OutputTypes 81 | manifestOutputType hls.OutputTypes 82 | baseOutPath string 83 | chunkBaseFilename string 84 | targetSegmentDurS float64 85 | chunkInitType ChunkInitTypes 86 | autoPIDs bool 87 | videoPID int 88 | audioPID int 89 | manifestType hls.ManifestTypes 90 | liveWindowSize int 91 | lhlsAdvancedChunks int 92 | httpClient *http.Client 93 | httpScheme string 94 | httpHost string 95 | } 96 | 97 | // ManifestGenerator Creates the manifest and chunks the media 98 | type ManifestGenerator struct { 99 | options options 100 | 101 | // Internal parsing data 102 | isInSync bool 103 | bytesToNextSync int 104 | detectedPMTID int 105 | 106 | // Current TS packet data 107 | tsPacket tspacket.TsPacket 108 | 109 | // Time counters 110 | chunkStartTimeS float64 111 | lastPCRS float64 112 | 113 | // Packet counter 114 | processedPackets uint64 115 | 116 | //currentChunks info (1 element array for HLS) 117 | currentChunks []mediachunk.Chunk 118 | currentChunkIndex uint64 119 | 120 | //currentChunk info 121 | initChunk *mediachunk.Chunk 122 | initState initStates 123 | 124 | // Packets used to save PAT and PMT (We know we'll break TS CC). Only used in ChunkInitStart mode 125 | tsInitPATPacket tspacket.TsPacket 126 | tsInitPMTPacket tspacket.TsPacket 127 | 128 | //Hls generator 129 | hlsChunklist hls.Hls 130 | 131 | //initialChunkCreation Flag tha indicates the first chunk[s] has been created 132 | fistChunkCreated bool 133 | } 134 | 135 | // New Creates a chunklistgenerator instance 136 | func New( 137 | log *logrus.Logger, 138 | chunkOutputType mediachunk.OutputTypes, 139 | manifestOutputType hls.OutputTypes, 140 | baseOutPath string, 141 | chunkBaseFilename string, 142 | chunkListFilename string, 143 | targetSegmentDurS float64, 144 | chunkInitType ChunkInitTypes, 145 | autoPIDs bool, 146 | videoPID int, 147 | audioPID int, 148 | manifestType hls.ManifestTypes, 149 | liveWindowSize int, 150 | lhlsAdvancedChunks int, 151 | httpClient *http.Client, 152 | httpScheme string, 153 | httpHost string, 154 | ) ManifestGenerator { 155 | if log == nil { 156 | log = logrus.New() 157 | log.SetLevel(logrus.DebugLevel) 158 | } 159 | 160 | chunklistFileName := path.Join(baseOutPath, chunkListFilename) 161 | 162 | mg := ManifestGenerator{ 163 | options{ 164 | log, 165 | chunkOutputType, 166 | manifestOutputType, 167 | baseOutPath, 168 | chunkBaseFilename, 169 | targetSegmentDurS, 170 | chunkInitType, 171 | autoPIDs, 172 | videoPID, 173 | audioPID, 174 | manifestType, 175 | liveWindowSize, 176 | lhlsAdvancedChunks, 177 | httpClient, 178 | httpScheme, 179 | httpHost, 180 | }, 181 | false, 182 | 0, 183 | -1, 184 | tspacket.New(tspacket.TsDefaultPacketSize), 185 | -1.0, 186 | -1.0, 187 | 0, 188 | nil, 189 | 0, 190 | nil, 191 | InitNotIni, 192 | tspacket.New(tspacket.TsDefaultPacketSize), 193 | tspacket.New(tspacket.TsDefaultPacketSize), 194 | hls.New( 195 | log, 196 | manifestType, 197 | HlsDefaultVersion, 198 | true, 199 | targetSegmentDurS, 200 | liveWindowSize+lhlsAdvancedChunks, 201 | chunklistFileName, 202 | "", 203 | manifestOutputType, 204 | httpClient, 205 | httpScheme, 206 | httpHost, 207 | ), 208 | false, 209 | } 210 | 211 | return mg 212 | } 213 | 214 | func (mg *ManifestGenerator) resync(buf []byte) []byte { 215 | mg.isInSync = false 216 | 217 | start := 0 218 | for { 219 | if start < len(buf) { 220 | if buf[start] == 0x47 { 221 | mg.isInSync = true 222 | break 223 | } else { 224 | start++ 225 | } 226 | } else { 227 | break 228 | } 229 | } 230 | 231 | return buf[start:] 232 | } 233 | 234 | func min(a, b int) int { 235 | if a < b { 236 | return a 237 | } 238 | 239 | return b 240 | } 241 | 242 | func (mg *ManifestGenerator) isSavingMediaPacket() bool { 243 | ret := false 244 | if !mg.options.autoPIDs { 245 | // Manual detection PIDs 246 | ret = true 247 | } else { 248 | if mg.options.chunkInitType == ChunkInit || mg.options.chunkInitType == ChunkInitStart { 249 | if mg.initState == InitsavedPMT { 250 | ret = true 251 | } 252 | } else if mg.options.chunkInitType == ChunkNoIni { 253 | ret = true 254 | } 255 | } 256 | 257 | return ret 258 | } 259 | 260 | func (mg *ManifestGenerator) saveInitPacket(tableType packetTableTypes) bool { 261 | if mg.options.chunkInitType == ChunkInit { 262 | return mg.addPacketToInitChunk(tableType) 263 | } else if mg.options.chunkInitType == ChunkInitStart { 264 | if tableType == PatTable || tableType == PmtTable { 265 | return mg.saveInitChunkPacket(tableType) 266 | } 267 | 268 | return false 269 | } 270 | 271 | return false 272 | } 273 | 274 | func (mg *ManifestGenerator) processPacket(forceChunk bool) bool { 275 | if !mg.tsPacket.Parse(mg.detectedPMTID) { 276 | return false 277 | } 278 | 279 | // Detect video & audio PIDs 280 | if mg.options.autoPIDs { 281 | pmtID := mg.tsPacket.GetPATdata() 282 | if pmtID >= 0 { 283 | mg.detectedPMTID = pmtID 284 | 285 | // Save PAT 286 | mg.saveInitPacket(PatTable) 287 | 288 | mg.options.log.Debug("Detected PAT. PMT ID: ", pmtID) 289 | } 290 | 291 | valid, Videoh264, AudioADTS, Other := mg.tsPacket.GetPMTdata() 292 | if valid { 293 | if len(Videoh264) > 0 { 294 | mg.options.videoPID = int(Videoh264[0]) 295 | } 296 | if len(AudioADTS) > 0 { 297 | mg.options.audioPID = int(AudioADTS[0]) 298 | } 299 | 300 | // Save PMT 301 | mg.saveInitPacket(PmtTable) 302 | 303 | mg.options.log.Debug("Detected PMT. VideoIDs: ", Videoh264, "AudiosIDs: ", AudioADTS, "Other: ", Other) 304 | } 305 | } 306 | 307 | pID := mg.tsPacket.GetPID() 308 | if pID == mg.options.videoPID { 309 | if mg.isSavingMediaPacket() { 310 | // Detect if we need to chunk it 311 | // It will chunk if detect an IDR point with PCR data 312 | if mg.tsPacket.IsRandomAccess(mg.options.videoPID) == true { 313 | mg.options.log.Debug("VIDEO: ", mg.tsPacket.String()) 314 | pcrS := mg.tsPacket.GetPCRS() 315 | if pcrS >= 0 { 316 | mg.lastPCRS = pcrS 317 | 318 | if mg.chunkStartTimeS < 0 && pcrS >= 0 { 319 | mg.chunkStartTimeS = pcrS 320 | } 321 | durS := pcrS - mg.chunkStartTimeS 322 | if (durS + ChunkLengthToleranceS) > mg.options.targetSegmentDurS { 323 | _, nextInitialPCRS := mg.nextChunk(pcrS, mg.chunkStartTimeS, tspacket.MaxPCRSValue, false) 324 | 325 | mg.chunkStartTimeS = nextInitialPCRS 326 | } 327 | } 328 | } 329 | mg.addPacketToChunk() 330 | 331 | } else { 332 | mg.options.log.Debug("SKIPPED VIDEO PACKET, not init: ", mg.tsPacket.String()) 333 | } 334 | } else if pID == mg.options.audioPID { 335 | if mg.isSavingMediaPacket() { 336 | mg.addPacketToChunk() 337 | mg.options.log.Debug("AUDIO: ", mg.tsPacket.String()) 338 | } else { 339 | mg.options.log.Debug("SKIPPED AUDIO PACKET, not init: ", mg.tsPacket.String()) 340 | } 341 | } else if pID >= 0 { 342 | mg.options.log.Debug("OTHER: ", mg.tsPacket.String()) 343 | } else { 344 | fmt.Println("OUT OF SYNC!!!") 345 | return false 346 | } 347 | 348 | return true 349 | } 350 | 351 | func (mg *ManifestGenerator) addPacketToChunk() { 352 | 353 | if mg.currentChunks == nil { 354 | mg.createChunk(false) 355 | } 356 | 357 | if len(mg.currentChunks) > 0 { 358 | 359 | //In case we need to save PAT and PMT do it just before the 1st packet 360 | if mg.options.chunkInitType == ChunkInitStart && mg.currentChunks[0].IsEmpty() { 361 | // Save PAT and PMT first if available 362 | if mg.initState == InitsavedPMT { 363 | mg.currentChunks[0].AddData(mg.tsInitPATPacket.GetBuffer()) 364 | mg.currentChunks[0].AddData(mg.tsInitPMTPacket.GetBuffer()) 365 | } 366 | } 367 | 368 | err := mg.currentChunks[0].AddData(mg.tsPacket.GetBuffer()) 369 | if err != nil { 370 | panic(err) 371 | } 372 | } 373 | } 374 | 375 | func (mg *ManifestGenerator) saveInitChunkPacket(tableType packetTableTypes) bool { 376 | ret := false 377 | 378 | if tableType == PatTable { 379 | if mg.initState == InitNotIni { 380 | // Save PAT 381 | mg.tsInitPATPacket = tspacket.CloneFrom(mg.tsPacket) 382 | mg.initState = InitsavedPAT 383 | ret = true 384 | } 385 | } else if tableType == PmtTable { 386 | if mg.initState == InitsavedPAT { 387 | // Save PMT 388 | mg.tsInitPMTPacket = tspacket.CloneFrom(mg.tsPacket) 389 | mg.initState = InitsavedPMT 390 | ret = true 391 | } 392 | } 393 | 394 | return ret 395 | } 396 | 397 | func (mg *ManifestGenerator) addPacketToInitChunk(tableType packetTableTypes) bool { 398 | ret := false 399 | saveData := false 400 | 401 | if tableType == PatTable { 402 | if mg.initState == InitNotIni { // We only save the 1st PAT PMT appeareance, so no dynamic updates are allowed 403 | if mg.initChunk == nil { 404 | // Create init chunk 405 | mg.createChunk(true) 406 | } 407 | saveData = true 408 | } 409 | } else if tableType == PmtTable { 410 | if mg.initState == InitsavedPAT { 411 | saveData = true 412 | } 413 | } 414 | 415 | if saveData { 416 | err := mg.initChunk.AddData(mg.tsPacket.GetBuffer()) 417 | if err != nil { 418 | panic(err) 419 | } 420 | 421 | if tableType == PatTable { 422 | mg.initState = InitsavedPAT 423 | } else if tableType == PmtTable { 424 | mg.initState = InitsavedPMT 425 | 426 | mg.closeChunk(true, -1, false) 427 | } 428 | 429 | ret = true 430 | } 431 | 432 | return ret 433 | } 434 | 435 | func (mg *ManifestGenerator) hlsClose() { 436 | mg.hlsChunklist.CloseManifest(true) 437 | } 438 | 439 | func (mg *ManifestGenerator) hlsAddChunk(isGrowing bool, fileName string, durationS float64, isDisco bool) { 440 | 441 | err := mg.hlsChunklist.AddChunk(hls.Chunk{IsGrowing: isGrowing, FileName: fileName, DurationS: durationS, IsDisco: isDisco}, true) 442 | if err != nil { 443 | mg.options.log.Error("Error generating / saving the chunklists. Err: ", err) 444 | } 445 | } 446 | 447 | func (mg *ManifestGenerator) closeChunk(isInit bool, chunkDurationS float64, isFinalChunk bool) { 448 | // Close current 449 | 450 | if isInit == false { 451 | if mg.currentChunks != nil && len(mg.currentChunks) > 0 { 452 | currentChunk := mg.currentChunks[0] 453 | 454 | currentChunk.Close() 455 | 456 | //NO LHLS 457 | if mg.options.lhlsAdvancedChunks <= 0 { 458 | mg.hlsAddChunk(false, currentChunk.GetFilename(), chunkDurationS, false) 459 | if mg.options.manifestType == hls.Vod { 460 | if isFinalChunk { 461 | mg.hlsClose() 462 | } 463 | } 464 | } 465 | 466 | if len(mg.currentChunks) > 1 { 467 | // Remove 1st element 468 | mg.currentChunks = mg.currentChunks[1:] 469 | } else { 470 | // Empty array 471 | mg.currentChunks = mg.currentChunks[:0] 472 | } 473 | 474 | mg.currentChunkIndex++ 475 | } 476 | } else { 477 | if mg.initChunk != nil { 478 | mg.initChunk.Close() 479 | 480 | mg.hlsChunklist.SetInitChunk(mg.initChunk.GetFilename()) 481 | 482 | // We need to update version 7 for map chunks 483 | mg.hlsChunklist.SetHlsVersion(7) 484 | 485 | mg.initChunk = nil 486 | } 487 | } 488 | 489 | return 490 | } 491 | 492 | func (mg *ManifestGenerator) createChunk(isInit bool) { 493 | // Close current 494 | if isInit { 495 | chunkInitOptions := mediachunk.Options{ 496 | Log: mg.options.log, 497 | OutputType: mg.options.chunkOutputType, 498 | LHLS: false, 499 | EstimatedDurationS: -1, 500 | FileNumberLength: ChunkFileNumberLength, 501 | GhostPrefix: GhostPrefixDefault, 502 | FileExtension: ChunkFileExtensionDefault, 503 | BasePath: mg.options.baseOutPath, 504 | ChunkBaseFilename: ChunkInitFileName, 505 | HTTPClient: mg.options.httpClient, 506 | HTTPScheme: mg.options.httpScheme, 507 | HTTPHost: mg.options.httpHost} 508 | 509 | newChunk := mediachunk.New(0, chunkInitOptions) 510 | mg.initChunk = &newChunk 511 | 512 | err := mg.initChunk.InitializeChunk() 513 | if err != nil { 514 | panic(err) 515 | } 516 | } else { 517 | chunksToCreate := 1 518 | if mg.fistChunkCreated == false && mg.options.lhlsAdvancedChunks > 0 { 519 | chunksToCreate = mg.options.lhlsAdvancedChunks 520 | mg.fistChunkCreated = true 521 | } 522 | 523 | n := 0 524 | for n < chunksToCreate { 525 | chunkOptions := mediachunk.Options{ 526 | Log: mg.options.log, 527 | OutputType: mg.options.chunkOutputType, 528 | LHLS: false, 529 | EstimatedDurationS: mg.options.targetSegmentDurS, 530 | FileNumberLength: ChunkFileNumberLength, 531 | GhostPrefix: GhostPrefixDefault, 532 | FileExtension: ChunkFileExtensionDefault, 533 | BasePath: mg.options.baseOutPath, 534 | ChunkBaseFilename: mg.options.chunkBaseFilename, 535 | HTTPClient: mg.options.httpClient, 536 | HTTPScheme: mg.options.httpScheme, 537 | HTTPHost: mg.options.httpHost} 538 | 539 | if mg.options.lhlsAdvancedChunks > 0 { 540 | chunkOptions.LHLS = true 541 | } 542 | 543 | newChunk := mediachunk.New(mg.currentChunkIndex+uint64(len(mg.currentChunks)), chunkOptions) 544 | 545 | err := newChunk.InitializeChunk() 546 | if err != nil { 547 | panic(err) 548 | } 549 | 550 | // Add the advanced chunk to the manifest with target dur 551 | if mg.options.lhlsAdvancedChunks > 0 { 552 | mg.hlsAddChunk(true, newChunk.GetFilename(), mg.options.targetSegmentDurS, false) 553 | } 554 | 555 | mg.currentChunks = append(mg.currentChunks, newChunk) 556 | 557 | n++ 558 | } 559 | } 560 | return 561 | } 562 | 563 | // Creates chunk and returns the initial time for the next chunk 564 | func (mg *ManifestGenerator) nextChunk(currentPCRS float64, lastInitialPCRS float64, maxPCRs float64, isFinalChunk bool) (chunkDurationS float64, nextInitialPCRS float64) { 565 | chunkDurationS = -1.0 566 | nextInitialPCRS = currentPCRS 567 | 568 | if currentPCRS >= lastInitialPCRS { 569 | chunkDurationS = currentPCRS - lastInitialPCRS 570 | } else { 571 | // Detected possible PCR roll over 572 | mg.options.log.Info("Possible PCR rollover! lastInitialPCRS:", lastInitialPCRS, ", currentPCRS: ", currentPCRS, ", maxPCRs: ", maxPCRs) 573 | chunkDurationS = maxPCRs - currentPCRS + lastInitialPCRS 574 | } 575 | 576 | mg.options.log.Info("CHUNK! At PCRs: ", currentPCRS, ". ChunkDurS: ", chunkDurationS) 577 | 578 | mg.closeChunk(false, chunkDurationS, isFinalChunk) 579 | if !isFinalChunk { 580 | mg.createChunk(false) 581 | } 582 | 583 | return 584 | } 585 | 586 | // Close Closes ManifestGenerator processing, saving last data and last chunk 587 | func (mg *ManifestGenerator) Close() { 588 | //Generate last chunk 589 | mg.nextChunk(mg.lastPCRS, mg.chunkStartTimeS, tspacket.MaxPCRSValue, true) 590 | } 591 | 592 | // AddData current chunk 593 | func (mg *ManifestGenerator) AddData(buf []byte) { 594 | if !mg.isInSync { 595 | buf = mg.resync(buf) 596 | 597 | if len(buf) > 0 { 598 | mg.bytesToNextSync = tspacket.TsDefaultPacketSize 599 | } 600 | } 601 | 602 | if len(buf) > 0 { 603 | addedSize := min(len(buf), mg.bytesToNextSync) 604 | mg.tsPacket.AddData(buf[:addedSize]) 605 | 606 | mg.bytesToNextSync = mg.bytesToNextSync - addedSize 607 | 608 | buf = buf[addedSize:] 609 | } 610 | 611 | if mg.bytesToNextSync <= 0 { 612 | // Process packet 613 | if mg.processPacket(false) == false { 614 | mg.isInSync = false 615 | } else { 616 | mg.bytesToNextSync = tspacket.TsDefaultPacketSize 617 | mg.processedPackets++ 618 | mg.tsPacket.Reset() 619 | } 620 | } 621 | 622 | if len(buf) > 0 { 623 | // Still data to process 624 | mg.AddData(buf[:]) 625 | } 626 | 627 | return 628 | } 629 | 630 | func (mg ManifestGenerator) getNumProcessedPackets() uint64 { 631 | return mg.processedPackets 632 | } 633 | --------------------------------------------------------------------------------