github.com/cnotch/ipchub@v1.1.0/av/format/hls/segmentgenerator.go (about)

     1  // Copyright calabashdad. https://github.com/calabashdad/seal.git
     2  //
     3  // Copyright (c) 2019,CAOHONGJU All rights reserved.
     4  // Use of this source code is governed by a MIT-style
     5  // license that can be found in the LICENSE file.
     6  
     7  package hls
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"path/filepath"
    13  	"strconv"
    14  
    15  	"github.com/cnotch/ipchub/av/format/mpegts"
    16  	"github.com/cnotch/ipchub/utils/murmur"
    17  	"github.com/cnotch/xlog"
    18  )
    19  
    20  // drop the segment when duration of ts too small.
    21  const hlsSegmentMinDurationMs = 100
    22  
    23  // in ms, for HLS aac flush the audio
    24  const hlsAacDelay = 100
    25  
    26  // SegmentGenerator generate the HLS ts segment.
    27  type SegmentGenerator struct {
    28  	playlist    *Playlist // 播放列表
    29  	path        string    // 流路径
    30  	hlsFragment int       // 每个片段长度
    31  
    32  	memory      bool   // 使用内存存储缓存到硬盘
    33  	segmentPath string // 缓存文件路径
    34  
    35  	sequenceNo int      // 片段序号
    36  	current    *segment //current segment
    37  
    38  	logger *xlog.Logger
    39  
    40  	audioRate   int
    41  	afCache     *mpegts.Frame // audio frame cache
    42  	afCacheBuff bytes.Buffer
    43  	// time jitter for aac
    44  	aacJitter *hlsAacJitter
    45  }
    46  
    47  // NewSegmentGenerator .
    48  func NewSegmentGenerator(playlist *Playlist, path string, hlsFragment int, segmentPath string, audioRate int, logger *xlog.Logger) (*SegmentGenerator, error) {
    49  	sg := &SegmentGenerator{
    50  		playlist:    playlist,
    51  		path:        path,
    52  		hlsFragment: hlsFragment,
    53  		memory:      segmentPath == "",
    54  		segmentPath: segmentPath,
    55  		logger:      logger,
    56  		sequenceNo:  0,
    57  		audioRate:   audioRate,
    58  		aacJitter:   newHlsAacJitter(),
    59  	}
    60  
    61  	if err := sg.segmentOpen(0); err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	// set the current segment to sequence header,
    66  	// when close the segement, it will write a discontinuity to m3u8 file.
    67  	sg.current.isSequenceHeader = true
    68  	return sg, nil
    69  }
    70  
    71  // open a new segment, a new ts file
    72  // segmentStartDts use to calc the segment duration, use 0 for the first segment of hls
    73  func (sg *SegmentGenerator) segmentOpen(segmentStartDts int64) (err error) {
    74  	if nil != sg.current {
    75  		// has already opened, ignore segment open
    76  		return
    77  	}
    78  
    79  	// new segment
    80  	sg.sequenceNo++
    81  	curr := newSegment(sg.memory)
    82  	curr.sequenceNo = sg.sequenceNo
    83  	curr.segmentStartPts = segmentStartDts
    84  	curr.uri = "/streams" + sg.path + "/" + strconv.Itoa(sg.sequenceNo) + ".ts"
    85  
    86  	tsFileName := fmt.Sprintf("%d_%d.ts", murmur.OfString(sg.path), curr.sequenceNo)
    87  	tsFilePath := filepath.Join(sg.segmentPath, tsFileName)
    88  	if err = curr.file.open(tsFilePath); err != nil {
    89  		return
    90  	}
    91  
    92  	sg.current = curr
    93  	return
    94  }
    95  
    96  // WriteMpegtsFrame implements mpegts.FrameWriter
    97  func (sg *SegmentGenerator) WriteMpegtsFrame(frame *mpegts.Frame) (err error) {
    98  	// if current is NULL, segment is not open, ignore the flush event.
    99  	if nil == sg.current {
   100  		return
   101  	}
   102  	if len(frame.Payload) <= 0 {
   103  		return
   104  	}
   105  
   106  	if frame.IsAudio() {
   107  		if sg.afCache == nil {
   108  			pts := sg.aacJitter.onBufferStart(frame.Pts, sg.audioRate)
   109  			headerFrame := *frame
   110  			headerFrame.Dts = pts
   111  			headerFrame.Pts = pts
   112  			sg.afCache = &headerFrame
   113  			sg.afCacheBuff.Write(frame.Payload)
   114  		} else {
   115  			sg.afCacheBuff.Write(frame.Header)
   116  			sg.afCacheBuff.Write(frame.Payload)
   117  			sg.aacJitter.onBufferContinue()
   118  		}
   119  
   120  		if frame.Pts-sg.afCache.Pts > hlsAacDelay*90 {
   121  			return sg.flushAudioCache()
   122  		}
   123  
   124  		// reap when current source is pure audio.
   125  		// it maybe changed when stream info changed,
   126  		// for example, pure audio when start, audio/video when publishing,
   127  		// pure audio again for audio disabled.
   128  		// so we reap event when the audio incoming when segment overflow.
   129  		// we use absolutely overflow of segment to make jwplayer/ffplay happy
   130  		if sg.isSegmentAbsolutelyOverflow() {
   131  			if err = sg.reapSegment(frame.Pts); err != nil {
   132  				return
   133  			}
   134  		}
   135  		return
   136  	}
   137  
   138  	if frame.IsKeyFrame() && sg.isSegmentOverflow() {
   139  		if err = sg.reapSegment(frame.Pts); err != nil {
   140  			return
   141  		}
   142  	}
   143  
   144  	// flush video when got one
   145  	if err = sg.flushFrame(frame); err != nil {
   146  		return
   147  	}
   148  	return
   149  }
   150  
   151  func (sg *SegmentGenerator) flushAudioCache() (err error) {
   152  	if sg.afCache == nil {
   153  		return
   154  	}
   155  
   156  	sg.afCache.Payload = sg.afCacheBuff.Bytes()
   157  	err = sg.flushFrame(sg.afCache)
   158  	sg.afCache = nil
   159  	sg.afCacheBuff.Reset()
   160  	return
   161  }
   162  
   163  func (sg *SegmentGenerator) flushFrame(frame *mpegts.Frame) (err error) {
   164  	sg.current.updateDuration(frame.Pts)
   165  	if err = sg.current.file.writeFrame(frame); err != nil {
   166  		return
   167  	}
   168  	return
   169  }
   170  
   171  // close segment(ts)
   172  func (sg *SegmentGenerator) segmentClose() (err error) {
   173  	if nil == sg.current {
   174  		return
   175  	}
   176  
   177  	curr := sg.current
   178  	sg.current = nil
   179  	curr.file.close()
   180  	if curr.duration*1000 < hlsSegmentMinDurationMs {
   181  		// reuse current segment index
   182  		sg.sequenceNo--
   183  		curr.file.delete()
   184  	} else {
   185  		sg.playlist.addSegment(curr)
   186  	}
   187  	return
   188  }
   189  
   190  // reopen the sg for a new hls segment,
   191  // close current segment, open a new segment,
   192  // then write the key frame to the new segment.
   193  // so, user must reap_segment then flush_video to hls sg.
   194  func (sg *SegmentGenerator) reapSegment(segmentStartDts int64) (err error) {
   195  	if err = sg.segmentClose(); err != nil {
   196  		return
   197  	}
   198  
   199  	if err = sg.segmentOpen(segmentStartDts); err != nil {
   200  		return
   201  	}
   202  
   203  	// segment open, flush the audio.
   204  	// @see: ngx_rtmp_hls_open_fragment
   205  	/* start fragment with audio to make iPhone happy */
   206  	err = sg.flushAudioCache()
   207  
   208  	return
   209  }
   210  
   211  // whether segment overflow,
   212  // that is whether the current segment duration>=(the segment in config)
   213  func (sg *SegmentGenerator) isSegmentOverflow() bool {
   214  	return sg.current.duration >= float64(sg.hlsFragment)
   215  }
   216  
   217  // whether segment absolutely overflow, for pure audio to reap segment,
   218  // that is whether the current segment duration>=2*(the segment in config)
   219  func (sg *SegmentGenerator) isSegmentAbsolutelyOverflow() bool {
   220  	if nil == sg.current {
   221  		return true
   222  	}
   223  
   224  	res := sg.current.duration >= float64(2*sg.hlsFragment)
   225  
   226  	return res
   227  }
   228  
   229  // Close .
   230  func (sg *SegmentGenerator) Close() error {
   231  	if nil == sg.current {
   232  		return nil
   233  	}
   234  
   235  	curr := sg.current
   236  	sg.current = nil
   237  	curr.file.close()
   238  	curr.file.delete()
   239  	return nil
   240  }