293 lines
8.3 KiB
Go
293 lines
8.3 KiB
Go
package laosiji
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
videoTranscodingVersion = "2.0"
|
|
transcodeSuccessStatus = 5
|
|
transcodeNoNeedStatus = 4
|
|
transcodeFailedStatus = -1
|
|
transcodeQueueLimit = 20
|
|
)
|
|
|
|
var errTranscodeTaskNotFound = errors.New("laosiji transcode task not found")
|
|
|
|
type uploadAPIResp struct {
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
Time string `json:"time"`
|
|
Error string `json:"error"`
|
|
ErrorCode int `json:"errorCode"`
|
|
}
|
|
|
|
type transcodeTask struct {
|
|
ID string `json:"id"`
|
|
FileID string `json:"file_id"`
|
|
FileURL string `json:"file_url"`
|
|
Status int `json:"status"`
|
|
Duration int `json:"duration"`
|
|
Height int `json:"height"`
|
|
Width int `json:"width"`
|
|
TranscodeError string `json:"transcode_error"`
|
|
TranscodeFile string `json:"transcode_file"`
|
|
PreviewVtt string `json:"preview_vtt"`
|
|
AudioFile string `json:"audio_file"`
|
|
SmartCover string `json:"smart_cover"`
|
|
PreviewDir string `json:"preview_dir"`
|
|
SrcMetaInfo *MetaInfo `json:"src_meta_info"`
|
|
HevcMetaInfo *MetaInfo `json:"hevc_meta_info"`
|
|
}
|
|
|
|
// MetaInfo 视频元信息(src_meta_info / hevc_meta_info 共用结构)。
|
|
type MetaInfo struct {
|
|
Codec string `json:"codec"`
|
|
Profile string `json:"profile"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Fps float64 `json:"fps"`
|
|
VideoBitrate int `json:"video_bitrate"`
|
|
PixFmt string `json:"pix_fmt"`
|
|
AudioCodec string `json:"audio_codec"`
|
|
AudioBitrate int `json:"audio_bitrate"`
|
|
AudioSampleRate int `json:"audio_sample_rate"`
|
|
AudioChannels int `json:"audio_channels"`
|
|
Bitrate int `json:"bitrate"`
|
|
Size int64 `json:"size"`
|
|
Duration float64 `json:"duration"`
|
|
}
|
|
|
|
type transcodeQueueInfo struct {
|
|
Waiting int `json:"waiting"`
|
|
Done int `json:"done"`
|
|
Error int `json:"error"`
|
|
}
|
|
|
|
// TranscodeResult 转码任务状态快照。
|
|
type TranscodeResult struct {
|
|
Status int
|
|
Done bool
|
|
Failed bool
|
|
HevcURL string
|
|
PreviewVtt string
|
|
AudioFile string
|
|
SmartCover string
|
|
HevcMeta *MetaInfo
|
|
}
|
|
|
|
// ResolveTranscodeFileID 优先使用媒资 fileId,缺失时回退为 fileURL 的 md5。
|
|
func ResolveTranscodeFileID(fileID, fileURL string) string {
|
|
if id := strings.TrimSpace(fileID); id != "" {
|
|
return id
|
|
}
|
|
return md5Hex(fileURL)
|
|
}
|
|
|
|
func (c *Client) SubmitTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains, videoId int64) (TranscodeResult, error) {
|
|
fileURL = strings.TrimSpace(fileURL)
|
|
if fileURL == "" {
|
|
return TranscodeResult{}, fmt.Errorf("empty transcode source url")
|
|
}
|
|
fileID = ResolveTranscodeFileID(fileID, fileURL)
|
|
|
|
task, err := c.queryVideoTranscoding(ctx, domains, fileID)
|
|
if err == nil {
|
|
return taskToResult(task), nil
|
|
}
|
|
if !errors.Is(err, errTranscodeTaskNotFound) {
|
|
return TranscodeResult{}, err
|
|
}
|
|
task, err = c.createVideoTranscoding(ctx, domains, fileID, fileURL, videoId)
|
|
if err != nil {
|
|
return TranscodeResult{}, err
|
|
}
|
|
return taskToResult(task), nil
|
|
}
|
|
|
|
func (c *Client) QueryTranscoding(ctx context.Context, fileURL, fileID string, domains SystemDomains) (result TranscodeResult, found bool, err error) {
|
|
fileURL = strings.TrimSpace(fileURL)
|
|
if fileURL == "" {
|
|
return TranscodeResult{}, false, fmt.Errorf("empty transcode source url")
|
|
}
|
|
fileID = ResolveTranscodeFileID(fileID, fileURL)
|
|
|
|
task, err := c.queryVideoTranscoding(ctx, domains, fileID)
|
|
if err != nil {
|
|
if errors.Is(err, errTranscodeTaskNotFound) {
|
|
return TranscodeResult{}, false, nil
|
|
}
|
|
return TranscodeResult{}, false, err
|
|
}
|
|
return taskToResult(task), true, nil
|
|
}
|
|
|
|
func (c *Client) createVideoTranscoding(ctx context.Context, domains SystemDomains, fileID, fileURL string, videoId int64) (transcodeTask, error) {
|
|
if q, err := c.getVideoTranscodingQueue(ctx, domains); err == nil && q.Waiting >= transcodeQueueLimit {
|
|
return transcodeTask{}, fmt.Errorf("h264toh265 queue full waiting:%d limit:%d", q.Waiting, transcodeQueueLimit)
|
|
}
|
|
endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding")
|
|
extData, _ := json.Marshal(map[string]interface{}{
|
|
"project": "ttrgw",
|
|
"url": fileURL,
|
|
"type": "h265",
|
|
"videoId": videoId,
|
|
})
|
|
return c.uploadAPIGet(ctx, endpoint, map[string]string{
|
|
"v": videoTranscodingVersion,
|
|
"key": domains.UploadKey,
|
|
"file_id": fileID,
|
|
"file_url": fileURL,
|
|
"notice_url": "",
|
|
"ext_data": string(extData),
|
|
})
|
|
}
|
|
|
|
func (c *Client) queryVideoTranscoding(ctx context.Context, domains SystemDomains, fileID string) (transcodeTask, error) {
|
|
endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding")
|
|
return c.uploadAPIGet(ctx, endpoint, map[string]string{
|
|
"v": videoTranscodingVersion,
|
|
"key": domains.UploadKey,
|
|
"file_id": fileID,
|
|
})
|
|
}
|
|
|
|
func taskToResult(task transcodeTask) TranscodeResult {
|
|
r := TranscodeResult{Status: task.Status}
|
|
switch {
|
|
case task.Status == transcodeSuccessStatus || task.Status == transcodeNoNeedStatus:
|
|
if task.Status == transcodeSuccessStatus {
|
|
if f := strings.TrimSpace(task.TranscodeFile); f != "" {
|
|
r.Done = true
|
|
r.HevcURL = joinURLPath("/laosiji/m3m", f)
|
|
}
|
|
} else {
|
|
r.Done = true
|
|
}
|
|
if r.Done {
|
|
r.PreviewVtt = laosijiPreviewURL(task.PreviewDir, task.PreviewVtt)
|
|
r.AudioFile = laosijiURL(task.AudioFile)
|
|
r.SmartCover = laosijiPreviewURL(task.PreviewDir, task.SmartCover)
|
|
r.HevcMeta = task.HevcMetaInfo
|
|
}
|
|
case task.Status <= transcodeFailedStatus:
|
|
r.Failed = true
|
|
}
|
|
return r
|
|
}
|
|
|
|
func laosijiURL(name string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
return joinURLPath("/laosiji", name)
|
|
}
|
|
|
|
func laosijiPreviewURL(previewDir, name string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
return joinURLPath(joinURLPath("/laosiji", strings.TrimSpace(previewDir)), name)
|
|
}
|
|
|
|
func (c *Client) getVideoTranscodingQueue(ctx context.Context, domains SystemDomains) (transcodeQueueInfo, error) {
|
|
endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo")
|
|
data, err := c.uploadAPICall(ctx, endpoint, map[string]string{
|
|
"v": videoTranscodingVersion,
|
|
"key": domains.UploadKey,
|
|
})
|
|
if err != nil {
|
|
return transcodeQueueInfo{}, err
|
|
}
|
|
var q transcodeQueueInfo
|
|
if len(data) > 0 {
|
|
if err = json.Unmarshal(data, &q); err != nil {
|
|
return transcodeQueueInfo{}, err
|
|
}
|
|
}
|
|
return q, nil
|
|
}
|
|
|
|
func (c *Client) uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) {
|
|
data, err := c.uploadAPICall(ctx, endpoint, params)
|
|
if err != nil {
|
|
return transcodeTask{}, err
|
|
}
|
|
var task transcodeTask
|
|
if len(data) > 0 {
|
|
if err = json.Unmarshal(data, &task); err != nil {
|
|
return transcodeTask{}, err
|
|
}
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
func (c *Client) uploadAPICall(ctx context.Context, endpoint string, params map[string]string) (json.RawMessage, error) {
|
|
parsed, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query := parsed.Query()
|
|
for k, v := range params {
|
|
query.Set(k, v)
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.hc.Do(httpReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, truncate(body, 300))
|
|
}
|
|
var apiResp uploadAPIResp
|
|
if err = json.Unmarshal(body, &apiResp); err != nil {
|
|
return nil, err
|
|
}
|
|
if apiResp.Status != "y" {
|
|
if apiResp.ErrorCode == 2000 {
|
|
return nil, errTranscodeTaskNotFound
|
|
}
|
|
return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, apiResp.Error)
|
|
}
|
|
return apiResp.Data, nil
|
|
}
|
|
|
|
func md5Hex(value string) string {
|
|
sum := md5.Sum([]byte(value))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func joinURLPath(host, uri string) string {
|
|
host = strings.TrimRight(strings.TrimSpace(host), "/")
|
|
uri = strings.TrimLeft(strings.TrimSpace(uri), "/")
|
|
if host == "" {
|
|
return uri
|
|
}
|
|
if uri == "" {
|
|
return host
|
|
}
|
|
return host + "/" + uri
|
|
}
|