You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lal/pkg/httpflv/client_pull_session.go

277 lines
7.6 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// Copyright 2019, Chef. All rights reserved.
// https://github.com/q191201771/lal
//
// Use of this source code is governed by a MIT-style license
// that can be found in the License file.
//
// Author: Chef (191201771@qq.com)
package httpflv
import (
"context"
"fmt"
"net"
"time"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/naza/pkg/nazahttp"
"github.com/q191201771/naza/pkg/connection"
"github.com/q191201771/naza/pkg/nazalog"
)
type PullSessionOption struct {
// 从调用Pull函数到接收音视频数据的前一步也即发送完HTTP请求的超时时间
// 如果为0则没有超时时间
PullTimeoutMS int
ReadTimeoutMS int // 接收数据超时单位毫秒如果为0则不设置超时
}
var defaultPullSessionOption = PullSessionOption{
PullTimeoutMS: 10000,
ReadTimeoutMS: 0,
}
type PullSession struct {
uniqueKey string // const after ctor
option PullSessionOption // const after ctor
conn connection.Connection
prevConnStat connection.Stat
staleStat *connection.Stat
stat base.StatSession
urlCtx base.URLContext
waitChan chan error
}
type ModPullSessionOption func(option *PullSessionOption)
func NewPullSession(modOptions ...ModPullSessionOption) *PullSession {
option := defaultPullSessionOption
for _, fn := range modOptions {
fn(&option)
}
uk := base.GenUKFLVPullSession()
s := &PullSession{
uniqueKey: uk,
option: option,
waitChan: make(chan error, 1),
}
nazalog.Infof("[%s] lifecycle new httpflv PullSession. session=%p", uk, s)
return s
}
type OnReadFLVTag func(tag Tag)
// 阻塞直到和对端完成拉流前握手部分的工作也即发送完HTTP Request或者发生错误
//
// @param rawURL 支持如下两种格式。(当然,关键点是对端支持)
// http://{domain}/{app_name}/{stream_name}.flv
// http://{ip}/{domain}/{app_name}/{stream_name}.flv
//
// @param onReadFLVTag 读取到 flv tag 数据时回调。回调结束后PullSession 不会再使用这块 <tag> 数据。
func (session *PullSession) Pull(rawURL string, onReadFLVTag OnReadFLVTag) error {
nazalog.Debugf("[%s] pull. url=%s", session.uniqueKey, rawURL)
var (
ctx context.Context
cancel context.CancelFunc
)
if session.option.PullTimeoutMS == 0 {
ctx, cancel = context.WithCancel(context.Background())
} else {
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(session.option.PullTimeoutMS)*time.Millisecond)
}
defer cancel()
return session.pullContext(ctx, rawURL, onReadFLVTag)
}
// 文档请参考: interface IClientSessionLifecycle
func (session *PullSession) Dispose() error {
nazalog.Infof("[%s] lifecycle dispose httpflv PullSession.", session.uniqueKey)
if session.conn == nil {
return base.ErrSessionNotStarted
}
return session.conn.Close()
}
// 文档请参考: interface IClientSessionLifecycle
func (session *PullSession) WaitChan() <-chan error {
return session.waitChan
}
// 文档请参考: interface ISessionURLContext
func (session *PullSession) URL() string {
return session.urlCtx.URL
}
// 文档请参考: interface ISessionURLContext
func (session *PullSession) AppName() string {
return session.urlCtx.PathWithoutLastItem
}
// 文档请参考: interface ISessionURLContext
func (session *PullSession) StreamName() string {
return session.urlCtx.LastItemOfPath
}
// 文档请参考: interface ISessionURLContext
func (session *PullSession) RawQuery() string {
return session.urlCtx.RawQuery
}
// 文档请参考: interface IObject
func (session *PullSession) UniqueKey() string {
return session.uniqueKey
}
// 文档请参考: interface ISessionStat
func (session *PullSession) UpdateStat(intervalSec uint32) {
currStat := session.conn.GetStat()
rDiff := currStat.ReadBytesSum - session.prevConnStat.ReadBytesSum
session.stat.ReadBitrate = int(rDiff * 8 / 1024 / uint64(intervalSec))
wDiff := currStat.WroteBytesSum - session.prevConnStat.WroteBytesSum
session.stat.WriteBitrate = int(wDiff * 8 / 1024 / uint64(intervalSec))
session.stat.Bitrate = session.stat.ReadBitrate
session.prevConnStat = currStat
}
// 文档请参考: interface ISessionStat
func (session *PullSession) GetStat() base.StatSession {
connStat := session.conn.GetStat()
session.stat.ReadBytesSum = connStat.ReadBytesSum
session.stat.WroteBytesSum = connStat.WroteBytesSum
return session.stat
}
// 文档请参考: interface ISessionStat
func (session *PullSession) IsAlive() (readAlive, writeAlive bool) {
currStat := session.conn.GetStat()
if session.staleStat == nil {
session.staleStat = new(connection.Stat)
*session.staleStat = currStat
return true, true
}
readAlive = !(currStat.ReadBytesSum-session.staleStat.ReadBytesSum == 0)
writeAlive = !(currStat.WroteBytesSum-session.staleStat.WroteBytesSum == 0)
*session.staleStat = currStat
return
}
func (session *PullSession) pullContext(ctx context.Context, rawURL string, onReadFLVTag OnReadFLVTag) error {
errChan := make(chan error, 1)
go func() {
if err := session.connect(rawURL); err != nil {
errChan <- err
return
}
if err := session.writeHTTPRequest(); err != nil {
errChan <- err
return
}
errChan <- nil
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errChan:
if err != nil {
return err
}
}
go session.runReadLoop(onReadFLVTag)
return nil
}
func (session *PullSession) connect(rawURL string) (err error) {
session.urlCtx, err = base.ParseHTTPFLVURL(rawURL, false)
if err != nil {
return
}
nazalog.Debugf("[%s] > tcp connect.", session.uniqueKey)
// # 建立连接
conn, err := net.Dial("tcp", session.urlCtx.HostWithPort)
if err != nil {
return err
}
session.conn = connection.New(conn, func(option *connection.Option) {
option.ReadBufSize = readBufSize
option.WriteTimeoutMS = session.option.ReadTimeoutMS // TODO chef: 为什么是 Read 赋值给 Write
option.ReadTimeoutMS = session.option.ReadTimeoutMS
})
return nil
}
func (session *PullSession) writeHTTPRequest() error {
// # 发送 http GET 请求
nazalog.Debugf("[%s] > W http request. GET %s", session.uniqueKey, session.urlCtx.PathWithRawQuery)
req := fmt.Sprintf("GET %s HTTP/1.0\r\nUser-Agent: %s\r\nAccept: */*\r\nRange: byte=0-\r\nConnection: close\r\nHost: %s\r\nIcy-MetaData: 1\r\n\r\n",
session.urlCtx.PathWithRawQuery, base.LALHTTPFLVPullSessionUA, session.urlCtx.StdHost)
_, err := session.conn.Write([]byte(req))
return err
}
func (session *PullSession) readHTTPRespHeader() (statusLine string, headers map[string]string, err error) {
// TODO chef: timeout
if statusLine, headers, err = nazahttp.ReadHTTPHeader(session.conn); err != nil {
return
}
_, code, _, err := nazahttp.ParseHTTPStatusLine(statusLine)
if err != nil {
return
}
nazalog.Debugf("[%s] < R http response header. code=%s", session.uniqueKey, code)
return
}
func (session *PullSession) readFLVHeader() ([]byte, error) {
flvHeader := make([]byte, flvHeaderSize)
_, err := session.conn.ReadAtLeast(flvHeader, flvHeaderSize)
if err != nil {
return flvHeader, err
}
nazalog.Debugf("[%s] < R http flv header.", session.uniqueKey)
// TODO chef: check flv header's value
return flvHeader, nil
}
func (session *PullSession) readTag() (Tag, error) {
return readTag(session.conn)
}
func (session *PullSession) runReadLoop(onReadFLVTag OnReadFLVTag) {
if _, _, err := session.readHTTPRespHeader(); err != nil {
session.waitChan <- err
return
}
if _, err := session.readFLVHeader(); err != nil {
session.waitChan <- err
return
}
for {
tag, err := session.readTag()
if err != nil {
session.waitChan <- err
return
}
onReadFLVTag(tag)
}
}