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/hls/server_handler.go

218 lines
6.1 KiB
Go

// Copyright 2020, 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 hls
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/q191201771/lal/pkg/base"
)
type IHlsServerHandlerObserver interface {
OnNewHlsSubSession(session *SubSession) error
OnDelHlsSubSession(session *SubSession)
}
type ServerHandler struct {
2 years ago
outPath string
observer IHlsServerHandlerObserver
urlPattern string
sessionMap map[string]*SubSession
mutex sync.Mutex
subSessionTimeout time.Duration
subSessionHashKey string
}
2 years ago
func NewServerHandler(outPath, urlPattern, subSessionHashKey string, subSessionTimeoutMs int, observer IHlsServerHandlerObserver) *ServerHandler {
if strings.HasPrefix(urlPattern, "/") {
urlPattern = urlPattern[1:]
}
sh := &ServerHandler{
2 years ago
outPath: outPath,
observer: observer,
urlPattern: urlPattern,
sessionMap: make(map[string]*SubSession),
subSessionTimeout: time.Duration(subSessionTimeoutMs) * time.Millisecond,
subSessionHashKey: subSessionHashKey,
}
go sh.runLoop()
return sh
}
func (s *ServerHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
urlCtx, err := base.ParseUrl(base.ParseHttpRequest(req), 80)
if err != nil {
Log.Errorf("parse url. err=%+v", err)
return
}
2 years ago
s.ServeHTTPWithUrlCtx(resp, req, urlCtx)
}
2 years ago
func (s *ServerHandler) ServeHTTPWithUrlCtx(resp http.ResponseWriter, req *http.Request, urlCtx base.UrlContext) {
//Log.Debugf("%+v", req)
2 years ago
var sessionIdHash string
var err error
urlObj, _ := url.Parse(urlCtx.Url)
// TODO chef:
// - check appname in URI path
filename := urlCtx.LastItemOfPath
filetype := urlCtx.GetFileType()
2 years ago
// 如果开启了hls sub session功能
if s.isSubSessionModeEnable() {
sessionIdHash = urlObj.Query().Get("session_id")
if filetype == "ts" && sessionIdHash != "" {
// 注意为了增强容错性不管是session_id字段无效还是session_id为空我们都依然返回ts文件内容给播放端
if sessionIdHash != "" {
err = s.keepSessionAlive(sessionIdHash)
if err != nil {
Log.Warnf("keepSessionAlive failed. session=%s, err=%+v", sessionIdHash, err)
}
} else {
// noop
}
} else if filetype == "m3u8" {
if sessionIdHash != "" {
err = s.keepSessionAlive(sessionIdHash)
if err != nil {
Log.Warnf("keepSessionAlive failed. session=%s, err=%+v", sessionIdHash, err)
}
} else {
// m3u8请求时session_id不存在创建session对象并让m3u8跳转到携带session_id的url请求
session, err := s.createSubSession(req, urlCtx)
if err != nil {
resp.WriteHeader(http.StatusNotFound)
return
}
query := urlObj.Query()
query.Set("session_id", session.sessionIdHash)
redirectUrl := urlObj.Path + "?" + query.Encode()
resp.Header().Add("Cache-Control", "no-cache")
2 years ago
base.AddCorsHeaders(resp)
http.Redirect(resp, req, redirectUrl, http.StatusFound)
return
}
}
}
ri := PathStrategy.GetRequestInfo(urlCtx, s.outPath)
//Log.Debugf("%+v", ri)
if filename == "" || (filetype != "m3u8" && filetype != "ts") || ri.StreamName == "" || ri.FileNameWithPath == "" {
err = errors.New(fmt.Sprintf("invalid hls request. url=%+v, request=%+v", urlCtx, ri))
Log.Warnf(err.Error())
resp.WriteHeader(http.StatusFound)
return
}
content, _err := ReadFile(ri.FileNameWithPath)
if _err != nil {
err = errors.New(fmt.Sprintf("read hls file failed. request=%+v, err=%+v", ri, _err))
Log.Warnf(err.Error())
resp.WriteHeader(http.StatusNotFound)
return
}
switch filetype {
case "m3u8":
resp.Header().Add("Content-Type", "application/x-mpegurl")
resp.Header().Add("Server", base.LalHlsM3u8Server)
2 years ago
// 给ts文件都携带上session_id字段
if sessionIdHash != "" {
content = bytes.ReplaceAll(content, []byte(".ts"), []byte(".ts?session_id="+sessionIdHash))
}
case "ts":
resp.Header().Add("Content-Type", "video/mp2t")
resp.Header().Add("Server", base.LalHlsTsServer)
}
resp.Header().Add("Cache-Control", "no-cache")
2 years ago
base.AddCorsHeaders(resp)
if sessionIdHash != "" {
session := s.getSubSession(sessionIdHash)
if session != nil {
session.AddWroteBytesSum(uint64(len(content)))
}
}
_, _ = resp.Write(content)
return
}
2 years ago
// getSubSession 获取 SubSession如果不存在返回nil
func (s *ServerHandler) getSubSession(sessionIdHash string) *SubSession {
s.mutex.Lock()
defer s.mutex.Unlock()
2 years ago
return s.sessionMap[sessionIdHash]
}
func (s *ServerHandler) createSubSession(req *http.Request, urlCtx base.UrlContext) (*SubSession, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
2 years ago
session := NewSubSession(req, urlCtx, s.urlPattern, s.subSessionHashKey, s.subSessionTimeout)
s.sessionMap[session.sessionIdHash] = session
2 years ago
if err := s.observer.OnNewHlsSubSession(session); err != nil {
delete(s.sessionMap, session.sessionIdHash)
return nil, err
}
return session, nil
}
2 years ago
// keepSessionAlive 标记延长session存活时间如果session不存在返回 base.ErrHlsSessionNotFound
func (s *ServerHandler) keepSessionAlive(sessionIdHash string) error {
s.mutex.Lock()
defer s.mutex.Unlock()
2 years ago
session := s.sessionMap[sessionIdHash]
if session == nil {
2 years ago
return base.ErrHlsSessionNotFound
}
2 years ago
session.KeepAlive()
return nil
}
func (s *ServerHandler) clearExpireSession() {
2 years ago
s.mutex.Lock()
defer s.mutex.Unlock()
for sessionIdHash, session := range s.sessionMap {
if session.IsExpired() {
2 years ago
delete(s.sessionMap, sessionIdHash)
s.observer.OnDelHlsSubSession(session)
}
}
}
2 years ago
func (s *ServerHandler) isSubSessionModeEnable() bool {
return s.subSessionHashKey != ""
}
func (s *ServerHandler) runLoop() {
2 years ago
// TODO(chef): [refactor] 也许可以弄到group中管理超时和其他协议的session管理方式保持一致 202211
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
s.clearExpireSession()
}
}
// m3u8文件用这个也行
//resp.Header().Add("Content-Type", "application/vnd.apple.mpegurl")