mirror of
https://github.com/lkmio/gb-cms.git
synced 2025-12-24 11:51:52 +08:00
fix: 还未生成截图, 访问应答404问题; 截图缩放到960*540大小;
This commit is contained in:
21
api/api.go
21
api/api.go
@@ -259,6 +259,22 @@ func (api *ApiServer) registerStatisticsHandler(actionName, path string, handler
|
||||
api.actionNames[path] = actionName
|
||||
}
|
||||
|
||||
type SnapshotFilter struct {
|
||||
}
|
||||
|
||||
func (f *SnapshotFilter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if "" != r.URL.Query().Get("preview_snapshot") {
|
||||
t := r.URL.Query().Get("t")
|
||||
ok := DefaultSnapshotManager.Get(t, 2*time.Second)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.FileServer(http.Dir("./snapshot")).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func StartApiServer(addr string) {
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_play", common.WithJsonParams(apiServer.OnPlay, &PlayDoneParams{}))
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_play_done", common.WithJsonParams(apiServer.OnPlayDone, &PlayDoneParams{}))
|
||||
@@ -268,7 +284,7 @@ func StartApiServer(addr string) {
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_receive_timeout", common.WithJsonParams(apiServer.OnReceiveTimeout, &StreamParams{}))
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_record", common.WithJsonParams(apiServer.OnRecord, &RecordParams{}))
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_started", apiServer.OnStarted)
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_snapshot", common.WithJsonParams(apiServer.OnSnapshot, &SnapshotParams{}))
|
||||
apiServer.router.HandleFunc("/api/v1/hook/on_snapshot", apiServer.OnSnapshot)
|
||||
|
||||
apiServer.registerStatisticsHandler("开始预览", "/api/v1/stream/start", withVerify(common.WithFormDataParams(apiServer.OnStreamStart, InviteParams{}))) // 实时预览
|
||||
apiServer.registerStatisticsHandler("停止预览", "/api/v1/stream/stop", withVerify(common.WithFormDataParams(apiServer.OnCloseLiveStream, InviteParams{}))) // 关闭实时预览
|
||||
@@ -411,7 +427,8 @@ func StartApiServer(addr string) {
|
||||
})
|
||||
|
||||
// 映射snapshot目录
|
||||
apiServer.router.PathPrefix("/snapshot/").Handler(http.StripPrefix("/snapshot/", http.FileServer(http.Dir("./snapshot"))))
|
||||
filter := SnapshotFilter{}
|
||||
apiServer.router.PathPrefix("/snapshot/").Handler(http.StripPrefix("/snapshot/", &filter))
|
||||
|
||||
// 前端路由
|
||||
htmlRoot := "./html/"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"gb-cms/stack"
|
||||
"github.com/csnewman/ffmpeg-go"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -234,11 +235,28 @@ func (api *ApiServer) OnStarted(_ http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnSnapshot(v *SnapshotParams, writer http.ResponseWriter, request *http.Request) {
|
||||
func (api *ApiServer) OnSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
if VideoKeyFrame2JPG == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1024*1024*2)
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if err.Error() == "http: request body too large" {
|
||||
http.Error(w, "file too large", http.StatusRequestEntityTooLarge)
|
||||
} else {
|
||||
http.Error(w, "read body failed", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
v := &SnapshotParams{}
|
||||
v.Stream = common.StreamID(r.Header.Get("stream"))
|
||||
v.Session = r.Header.Get("session")
|
||||
v.Codec = r.Header.Get("codec")
|
||||
v.KeyFrameData = data
|
||||
|
||||
var codecId ffmpeg.AVCodecID
|
||||
switch strings.ToLower(v.Codec) {
|
||||
case "h264":
|
||||
@@ -250,8 +268,8 @@ func (api *ApiServer) OnSnapshot(v *SnapshotParams, writer http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
jpgPath := GetSnapshotPath(v.Stream, v.Session)
|
||||
err := os.MkdirAll(path.Dir(jpgPath), 0755)
|
||||
jpgPath := GetSnapshotPath(v.Stream)
|
||||
err = os.MkdirAll(path.Dir(jpgPath), 0755)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("创建目录失败 err: %s", err.Error())
|
||||
return
|
||||
@@ -261,16 +279,18 @@ func (api *ApiServer) OnSnapshot(v *SnapshotParams, writer http.ResponseWriter,
|
||||
err = VideoKeyFrame2JPG(codecId, v.KeyFrameData, jpgPath)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("转换为JPEG失败 err: %s", err.Error())
|
||||
} else if err = dao.Channel.SetSnapshotPath(v.Stream.DeviceID(), v.Stream.ChannelID(), jpgPath); err != nil {
|
||||
} else if err = dao.Channel.SetSnapshotPath(v.Stream.DeviceID(), v.Stream.ChannelID(), jpgPath+"?t="+v.Session); err != nil {
|
||||
// 数据库更新通道的最新截图
|
||||
log.Sugar.Errorf("更新通道最新截图失败 err: %s", err.Error())
|
||||
} else {
|
||||
DefaultSnapshotManager.Put(v.Session, true)
|
||||
}
|
||||
}
|
||||
|
||||
func GetSnapshotPath(streamID common.StreamID, sessionID string) string {
|
||||
func GetSnapshotPath(streamID common.StreamID) string {
|
||||
if VideoKeyFrame2JPG == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return path.Join("./snapshot/", string(streamID), sessionID+".jpg")
|
||||
return path.Join("./snapshot/", string(streamID)+".jpg")
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (api *ApiServer) DoStartStream(v *InviteParams, w http.ResponseWriter, r *h
|
||||
RecordStartAt: "",
|
||||
RelaySize: 0,
|
||||
SMSID: "",
|
||||
SnapURL: GetSnapshotPath(stream.StreamID, stream.SessionID),
|
||||
SnapURL: GetSnapshotPath(stream.StreamID) + "?preview_snapshot=1&t=" + stream.SessionID,
|
||||
SourceAudioCodecName: "",
|
||||
SourceAudioSampleRate: 0,
|
||||
SourceVideoCodecName: "",
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/csnewman/ffmpeg-go"
|
||||
"github.com/disintegration/imaging"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -111,9 +113,41 @@ func saveFrameAsJPEG(frame *ffmpeg.AVFrame, filename string) error {
|
||||
return fmt.Errorf("接收包失败 %v", err)
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if _, err := file.Write(unsafe.Slice((*byte)(pkt.Data()), pkt.Size())); err != nil {
|
||||
return fmt.Errorf("写入文件失败 %v", err)
|
||||
// 获取编码后的 JPEG 原始字节数据
|
||||
jpegBytes := unsafe.Slice((*byte)(pkt.Data()), pkt.Size())
|
||||
|
||||
// 判断是否需要缩放
|
||||
targetW, targetH := 960, 540
|
||||
if (frame.Width() > frame.Height()) && frame.Width() > targetW || frame.Height() > targetH {
|
||||
// [路径 A: 需要缩放]
|
||||
|
||||
// 1. 将 JPEG 字节流解码为 Go Image 对象
|
||||
img, err := imaging.Decode(bytes.NewReader(jpegBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Go解码图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 使用 imaging 库进行缩放 (Fit 会保持比例,适应 960x540 的框)
|
||||
// 使用 Lanczos 算法保证缩放后的清晰度
|
||||
dstImg := imaging.Fit(img, targetW, targetH, imaging.Lanczos)
|
||||
|
||||
// 3. 保存缩放后的图片到文件
|
||||
if err := imaging.Save(dstImg, filename, imaging.JPEGQuality(80)); err != nil {
|
||||
return fmt.Errorf("保存缩放图片失败: %v", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// [路径 B: 不需要缩放]
|
||||
// 直接将 FFmpeg 编码好的字节写入文件,效率最高
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件失败 %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(jpegBytes); err != nil {
|
||||
return fmt.Errorf("写入文件失败 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
63
api/snapshot_async.go
Normal file
63
api/snapshot_async.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SnapshotQueueMaxSize = 128
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultSnapshotManager = &SnapshotManager{
|
||||
snapshots: make(map[string][]func(), SnapshotQueueMaxSize*1.5),
|
||||
keys: list.New(),
|
||||
}
|
||||
)
|
||||
|
||||
type SnapshotManager struct {
|
||||
lock sync.RWMutex
|
||||
snapshots map[string][]func()
|
||||
keys *list.List
|
||||
}
|
||||
|
||||
func (s *SnapshotManager) Get(id string, timeout time.Duration) bool {
|
||||
s.lock.Lock()
|
||||
if _, ok := s.snapshots[id]; ok {
|
||||
s.lock.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
background, cancel := context.WithCancel(context.Background())
|
||||
s.snapshots[id] = append(s.snapshots[id], cancel)
|
||||
// 超时未获取到截图, 则认为不存在
|
||||
after := time.After(timeout)
|
||||
|
||||
s.lock.Unlock()
|
||||
select {
|
||||
case <-after:
|
||||
return false
|
||||
case <-background.Done():
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotManager) Put(id string, ok bool) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// 通知获取截图的goroutine, 截图已准备好
|
||||
for _, cancel := range s.snapshots[id] {
|
||||
cancel()
|
||||
}
|
||||
|
||||
// 超出最大队列长度, 则删除最早的截图
|
||||
for s.keys.Len() > SnapshotQueueMaxSize {
|
||||
oldElement := s.keys.Front()
|
||||
s.keys.Remove(oldElement)
|
||||
delete(s.snapshots, oldElement.Value.(string))
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -43,6 +43,7 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
@@ -54,6 +55,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/csnewman/ffmpeg-go v0.6.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.6
|
||||
github.com/gorilla/mux v1.8.1
|
||||
|
||||
Reference in New Issue
Block a user