添加音频推送
This commit is contained in:
parent
e222d8b3c5
commit
70a62c2093
2
.gitignore
vendored
2
.gitignore
vendored
@ -67,5 +67,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
uploads/*
|
||||
dish*
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 647 KiB |
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.img-box {
|
||||
max-width: 260px;
|
||||
max-width: 660px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 2.2 MiB |
10
go.mod
10
go.mod
@ -6,11 +6,16 @@ require (
|
||||
git.lnton.com/lnton/pkg v1.5.27
|
||||
github.com/gin-contrib/static v1.1.5
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/ixugo/goddd v1.4.0
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pion/rtp v1.9.0
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
|
||||
github.com/youpy/go-wav v0.3.2
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
@ -24,8 +29,6 @@ require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafov/m3u8 v0.12.1 // indirect
|
||||
github.com/ixugo/netpulse v0.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@ -34,12 +37,15 @@ require (
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.11.3 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 // indirect
|
||||
github.com/youpy/go-riff v0.1.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
|
||||
|
||||
75
go.sum
75
go.sum
@ -1,34 +1,20 @@
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
git.lnton.com/lnton/pkg v1.5.27 h1:pf4QqL00/yrGtVUdaHo7eZ941D6B9q5oX4ffnjAYWI4=
|
||||
git.lnton.com/lnton/pkg v1.5.27/go.mod h1:+xvqNpqlxuRthZHiuaMg5Spf5yIgRE63KrmOFLmlp3E=
|
||||
git.lnton.com/lnton/protocol v0.0.18 h1:QyhNt/J924PaEBz/LIUYOtUiz4Hkp1AMeuZvPa/5W1M=
|
||||
git.lnton.com/lnton/protocol v0.0.18/go.mod h1:gSXM43E9v4KnCI3FN6iaIB+acvPF6tj+E+m9XekakjY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
@ -41,7 +27,6 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
@ -62,24 +47,20 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s=
|
||||
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/ixugo/goddd v1.4.0 h1:2SMO7wfqVYuZu5KwdvN6aLk1cOY9MgxcJAhClH1yhMQ=
|
||||
github.com/ixugo/goddd v1.4.0/go.mod h1:FzEjEd6uWEWan1XWTh8VXdqGtyjMYGow/URNtBY8X7w=
|
||||
github.com/ixugo/netpulse v0.1.1 h1:M7pdwJhpSDuwFdjEgCcanR5lLZgd+4akOstgbyRZOgw=
|
||||
@ -102,11 +83,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
|
||||
@ -117,69 +95,64 @@ github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYr
|
||||
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtp v1.9.0 h1:NL2nGZPXhjnTQGRgsDZRv0ZTo0Or5fkjCy9o9PtBHBU=
|
||||
github.com/pion/rtp v1.9.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/wenlng/go-captcha-assets v1.0.1/go.mod h1:yQqc7rRbxgLCg+tWtVp+7Y317D1wIZDan/yIwt8wSac=
|
||||
github.com/wenlng/go-captcha/v2 v2.0.2/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
||||
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634=
|
||||
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
@ -206,12 +179,10 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
|
||||
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -219,20 +190,15 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
@ -240,7 +206,6 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@ -248,21 +213,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
modernc.org/cc/v4 v4.24.1 h1:mLykA8iIlZ/SZbwI2JgYIURXQMSgmOb/+5jaielxPi4=
|
||||
modernc.org/cc/v4 v4.24.1/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
|
||||
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
|
||||
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
|
||||
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
|
||||
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.61.5 h1:WzsPUvWl2CvsRmk2foyWWHUEUmQ2iW4oFyWOVR0O5ho=
|
||||
modernc.org/libc v1.61.5/go.mod h1:llBdEGIywhnRgAFuTF+CWaKV8/2bFgACcQZTXhkAuAM=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
@ -279,4 +241,3 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@ -49,7 +49,7 @@ func Run(bc *conf.Bootstrap) {
|
||||
server.ReadTimeout(bc.Server.HTTP.Timeout.Duration()),
|
||||
server.WriteTimeout(bc.Server.HTTP.Timeout.Duration()),
|
||||
)
|
||||
lis, err := net.Listen("tcp", ":8089")
|
||||
lis, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
fmt.Printf("创建监听器失败: %v\n", err)
|
||||
return
|
||||
|
||||
@ -17,7 +17,7 @@ type AudioEncodeStorer interface {
|
||||
Add(context.Context, *AudioEncode) error
|
||||
Edit(context.Context, *AudioEncode, func(*AudioEncode), ...orm.QueryOption) error
|
||||
Del(context.Context, *AudioEncode, ...orm.QueryOption) error
|
||||
EditEnable(enable bool, id int) error
|
||||
EditStatus(status EncodeStatus, id int) error
|
||||
}
|
||||
|
||||
// FindAudioEncodeAll Paginated search
|
||||
@ -34,13 +34,16 @@ func (c Core) FindAudioEncodeAll() ([]*AudioEncode, int64, error) {
|
||||
func (c Core) FindAudioEncode(ctx context.Context, in *FindAudioEncodeInput) ([]*AudioEncode, int64, error) {
|
||||
items := make([]*AudioEncode, 0)
|
||||
if in.Name != "" {
|
||||
total, err := c.store.AudioEncode().Find(ctx, &items, in, orm.Where("name like ? ", "%"+in.Name+"%"))
|
||||
query := orm.NewQuery(8).
|
||||
Where("name like ? ", "%"+in.Name+"%").OrderBy("created_at DESC")
|
||||
total, err := c.store.AudioEncode().Find(ctx, &items, in, query.Encode()...)
|
||||
if err != nil {
|
||||
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
|
||||
}
|
||||
return items, total, nil
|
||||
} else {
|
||||
total, err := c.store.AudioEncode().Find(ctx, &items, in)
|
||||
query := orm.NewQuery(2).OrderBy("created_at DESC")
|
||||
total, err := c.store.AudioEncode().Find(ctx, &items, in, query.Encode()...)
|
||||
if err != nil {
|
||||
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
|
||||
}
|
||||
@ -104,9 +107,9 @@ func (c Core) DelAudioEncode(ctx context.Context, id int) (*AudioEncode, error)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AudioEncodeEnable Update
|
||||
func (c Core) AudioEncodeEnable(enable bool, id int) error {
|
||||
if err := c.store.AudioEncode().EditEnable(enable, id); err != nil {
|
||||
// AudioEncodeStatus Update
|
||||
func (c Core) AudioEncodeStatus(status EncodeStatus, id int) error {
|
||||
if err := c.store.AudioEncode().EditStatus(status, id); err != nil {
|
||||
return reason.ErrDB.Withf(`Enable err[%s]`, err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -31,5 +31,5 @@ type AddAudioEncodeInput struct {
|
||||
Mode string `json:"mode"` // 文件类型 mp3 wav
|
||||
Size int64 `json:"size"` // 文件大小
|
||||
Des string `json:"des"` // 描述
|
||||
|
||||
Duration int64 `json:"duration"` // 时长
|
||||
}
|
||||
|
||||
133
internal/core/audioencode/audiotask.go
Normal file
133
internal/core/audioencode/audiotask.go
Normal file
@ -0,0 +1,133 @@
|
||||
// Code generated by gowebx, DO AVOID EDIT.
|
||||
package audioencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.lnton.com/lnton/pkg/orm"
|
||||
"git.lnton.com/lnton/pkg/reason"
|
||||
"github.com/jinzhu/copier"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// AudioTaskStorer Instantiation interface
|
||||
type AudioTaskStorer interface {
|
||||
Find(context.Context, *[]*AudioTask, orm.Pager, ...orm.QueryOption) (int64, error)
|
||||
FindAll(dp *[]*AudioTask) (int64, error)
|
||||
Get(context.Context, *AudioTask, ...orm.QueryOption) error
|
||||
Add(context.Context, *AudioTask) error
|
||||
Edit(context.Context, *AudioTask, func(*AudioTask), ...orm.QueryOption) error
|
||||
Del(context.Context, *AudioTask, ...orm.QueryOption) error
|
||||
EditStatus(status int, id int) error
|
||||
EditStatusError(id, status int, s string) error
|
||||
}
|
||||
|
||||
// FindAudioTaskAll Paginated search
|
||||
func (c Core) FindAudioTaskAll() ([]*AudioTask, int64, error) {
|
||||
items := make([]*AudioTask, 0)
|
||||
total, err := c.store.AudioTask().FindAll(&items)
|
||||
if err != nil {
|
||||
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// FindAudioTask Paginated search
|
||||
func (c Core) FindAudioTask(ctx context.Context, in *FindAudioTaskInput) ([]*AudioTask, int64, error) {
|
||||
items := make([]*AudioTask, 0)
|
||||
if in.Name != "" {
|
||||
query := orm.NewQuery(8).
|
||||
Where("audio_name like ? OR channel_id like ? OR channel_name like ?", "%"+in.Name+"%", "%"+in.Name+"%", "%"+in.Name+"%").OrderBy("created_at DESC")
|
||||
total, err := c.store.AudioTask().Find(ctx, &items, in, query.Encode()...)
|
||||
if err != nil {
|
||||
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
|
||||
}
|
||||
return items, total, nil
|
||||
} else {
|
||||
query := orm.NewQuery(2).OrderBy("created_at DESC")
|
||||
total, err := c.store.AudioTask().Find(ctx, &items, in, query.Encode()...)
|
||||
if err != nil {
|
||||
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetAudioTask Query a single object
|
||||
func (c Core) GetAudioTask(ctx context.Context, id int) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := c.store.AudioTask().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
|
||||
if orm.IsErrRecordNotFound(err) {
|
||||
return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
|
||||
}
|
||||
return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
func (c Core) GetNameAudioTask(ctx context.Context, name string) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := c.store.AudioTask().Get(ctx, &out, orm.Where("name=?", name)); err != nil {
|
||||
if orm.IsErrRecordNotFound(err) {
|
||||
return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
|
||||
}
|
||||
return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AddAudioTask Insert into database
|
||||
func (c Core) AddAudioTask(ctx context.Context, in *AddAudioTask) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := copier.Copy(&out, in); err != nil {
|
||||
slog.Error("Copy", "err", err)
|
||||
}
|
||||
if err := c.store.AudioTask().Add(ctx, &out); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// EditAudioTask Update object information
|
||||
func (c Core) EditAudioTask(ctx context.Context, in *EditAudioTaskInput, id int) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := c.store.AudioTask().Edit(ctx, &out, func(b *AudioTask) {
|
||||
if err := copier.Copy(b, in); err != nil {
|
||||
slog.Error("Copy", "err", err)
|
||||
}
|
||||
}, orm.Where("id=?", id)); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DelAudioTask Delete object
|
||||
func (c Core) DelAudioTask(ctx context.Context, id int) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := c.store.AudioTask().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Del err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c Core) DelAudioTaskAll(ctx context.Context, ids []int) (*AudioTask, error) {
|
||||
var out AudioTask
|
||||
if err := c.store.AudioTask().Del(ctx, &out, orm.Where("id in (?)", ids)); err != nil {
|
||||
return nil, reason.ErrDB.Withf(`Del ids err[%s]`, err.Error())
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AudioTaskStatus Update
|
||||
func (c Core) AudioTaskStatus(status int, id int) error {
|
||||
if err := c.store.AudioTask().EditStatus(status, id); err != nil {
|
||||
return reason.ErrDB.Withf(`Status err[%s]`, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AudioTaskStatus Update
|
||||
func (c Core) AudioTaskStatusError(id, status int, s string) error {
|
||||
if err := c.store.AudioTask().EditStatusError(id, status, s); err != nil {
|
||||
return reason.ErrDB.Withf(`StatusError err[%s]`, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
internal/core/audioencode/audiotask.param.go
Normal file
47
internal/core/audioencode/audiotask.param.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Code generated by gowebx, DO AVOID EDIT.
|
||||
package audioencode
|
||||
|
||||
import (
|
||||
"git.lnton.com/lnton/pkg/web"
|
||||
)
|
||||
|
||||
type FindAudioTaskInput struct {
|
||||
web.PagerFilter
|
||||
Name string `form:"name"` // 名称
|
||||
AudioID int `form:"audio_id"` // 关联音频
|
||||
AudioName string `form:"audio_name"` // 音频名称
|
||||
ChannelID string `form:"channel_id"` // 关联通道
|
||||
ChannelName string `form:"channel_name"` // 拉流地址
|
||||
|
||||
}
|
||||
type EditAudioTaskInput struct {
|
||||
AudioID int `json:"audio_id"` // 关联音频
|
||||
ChannelID string `json:"channel_id"` // 关联通道
|
||||
ChannelName string `json:"channel_name"` // 关联通道名称
|
||||
}
|
||||
type AddAudioTaskItem struct {
|
||||
AudioID int `json:"audio_id"` // 关联音频
|
||||
ChannelID string `json:"channel_id"` // 关联通道
|
||||
ChannelName string `json:"channel_name"` // 关联通道名称
|
||||
}
|
||||
type AddErrorOutput struct {
|
||||
AudioID int `json:"audio_id"` // 关联音频
|
||||
ChannelID string `json:"channel_id"` // 关联通道
|
||||
ChannelName string `json:"channel_name"` // 关联通道名称
|
||||
ErrorMsg string `json:"error_msg"` // 错误信息
|
||||
}
|
||||
type AddAudioTaskInput struct {
|
||||
Items []AddAudioTaskItem `json:"items"`
|
||||
}
|
||||
type AddAudioTask struct {
|
||||
AudioID int `json:"audio_id"` // 关联音频
|
||||
ChannelID string `json:"channel_id"` // 关联通道
|
||||
ChannelName string `json:"channel_name"` // 关联通道名称
|
||||
AudioName string `json:"audio_name"` // 音频名称
|
||||
Mode string `json:"mode"` // 文件类型 mp3 wav
|
||||
Size int64 `json:"size"` // 文件大小
|
||||
Duration int64 `json:"duration"` // 文件时长
|
||||
}
|
||||
type DelAudioTaskInput struct {
|
||||
IDs []int `json:"ids"` // id
|
||||
}
|
||||
@ -4,6 +4,7 @@ package audioencode
|
||||
// Storer data
|
||||
type Storer interface {
|
||||
AudioEncode() AudioEncodeStorer
|
||||
AudioTask() AudioTaskStorer
|
||||
}
|
||||
|
||||
// Core
|
||||
|
||||
@ -3,6 +3,7 @@ package audioencode
|
||||
|
||||
import (
|
||||
"git.lnton.com/lnton/pkg/orm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EncodeStatus int
|
||||
@ -23,6 +24,7 @@ type AudioEncode struct {
|
||||
EncodeStatus EncodeStatus `gorm:"column:encode_status;notNull;default:0;comment:转码状态" json:"encode_status"` // 转码状态
|
||||
Mode string `gorm:"column:mode;notNull;default:'';comment:文件类型" json:"mode"` // 文件类型 mp3 wav
|
||||
Size int64 `gorm:"column:size;notNull;default:0;comment:文件大小" json:"size"` // 文件大小
|
||||
Duration int64 `gorm:"column:duration;notNull;default:0;comment:文件时长" json:"duration"` // 文件时长
|
||||
Des string `gorm:"column:des;notNull;default:'';comment:描述" json:"des"` // 描述
|
||||
}
|
||||
|
||||
@ -30,3 +32,25 @@ type AudioEncode struct {
|
||||
func (*AudioEncode) TableName() string {
|
||||
return "audio_encode"
|
||||
}
|
||||
|
||||
type AudioTask struct {
|
||||
orm.Model
|
||||
AudioID int `gorm:"column:audio_id;notNull;default:0;comment:关联音频" json:"audio_id"` // 关联音频
|
||||
ChannelID string `gorm:"column:channel_id;notNull;default:'';comment:关联通道" json:"channel_id"` // 关联通道
|
||||
ChannelName string `gorm:"column:channel_name;notNull;default:'';comment:关联通道名称" json:"channel_name"` // 关联通道名称
|
||||
|
||||
AudioName string `gorm:"column:audio_name;notNull;default:'';comment:音频名称" json:"audio_name"` // 音频名称
|
||||
Mode string `gorm:"column:mode;notNull;default:'';comment:文件类型" json:"mode"` // 文件类型 mp3 wav
|
||||
Size int64 `gorm:"column:size;notNull;default:0;comment:文件大小" json:"size"` // 文件大小
|
||||
Duration int64 `gorm:"column:duration;notNull;default:0;comment:文件时长" json:"duration"` // 文件时长
|
||||
TaskStatus int `gorm:"column:task_status;notNull;default:0;comment:任务状态" json:"task_status"` // 任务状态
|
||||
CreateTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:创建时间" json:"created_time"` // 任务创建时间
|
||||
StartTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:开始时间" json:"start_time"` // 任务开始时间
|
||||
EndTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:结束时间" json:"end_time"` // 任务结束时间
|
||||
ErrorMsg string `gorm:"column:error_msg;notNull;default:'';comment:错误信息" json:"error_msg"` // 错误信息
|
||||
}
|
||||
|
||||
// TableName database table name
|
||||
func (*AudioTask) TableName() string {
|
||||
return "audio_task"
|
||||
}
|
||||
|
||||
@ -48,9 +48,9 @@ func (d AudioEncode) Del(ctx context.Context, model *audioencode.AudioEncode, op
|
||||
return orm.DeleteWithContext(ctx, d.db, model, opts...)
|
||||
}
|
||||
|
||||
// EditEnable implements audioencode.AudioEncodeStorer.
|
||||
func (d AudioEncode) EditEnable(enable bool, id int) error {
|
||||
if err := d.db.Model(&audioencode.AudioEncode{}).Where(`id = ?`, id).Update("enable", enable).Error; err != nil {
|
||||
// EditStatus implements audioencode.AudioEncodeStorer.
|
||||
func (d AudioEncode) EditStatus(status audioencode.EncodeStatus, id int) error {
|
||||
if err := d.db.Model(&audioencode.AudioEncode{}).Where(`id = ?`, id).Update("encode_status", status).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
68
internal/core/audioencode/store/audioencodedb/audiotask.go
Normal file
68
internal/core/audioencode/store/audioencodedb/audiotask.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Code generated by gowebx, DO AVOID EDIT.
|
||||
package audioencodedb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"easyaudioencode/internal/core/audioencode"
|
||||
"git.lnton.com/lnton/pkg/orm"
|
||||
)
|
||||
|
||||
var _ audioencode.AudioTaskStorer = AudioTask{}
|
||||
|
||||
// AudioTask Related business namespaces
|
||||
type AudioTask DB
|
||||
|
||||
// FindAll implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) FindAll(bs *[]*audioencode.AudioTask) (int64, error) {
|
||||
db := d.db.Model(&audioencode.AudioTask{})
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil || total <= 0 {
|
||||
// 如果统计失败或者数量为0,则返回错误
|
||||
return 0, err
|
||||
}
|
||||
return total, db.Find(bs).Error
|
||||
}
|
||||
|
||||
// Find implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) Find(ctx context.Context, bs *[]*audioencode.AudioTask, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
|
||||
return orm.FindWithContext(ctx, d.db, bs, page, opts...)
|
||||
}
|
||||
|
||||
// Get implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) Get(ctx context.Context, model *audioencode.AudioTask, opts ...orm.QueryOption) error {
|
||||
return orm.FirstWithContext(ctx, d.db, model, opts...)
|
||||
}
|
||||
|
||||
// Add implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) Add(ctx context.Context, model *audioencode.AudioTask) error {
|
||||
return d.db.WithContext(ctx).Create(model).Error
|
||||
}
|
||||
|
||||
// Edit implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) Edit(ctx context.Context, model *audioencode.AudioTask, changeFn func(*audioencode.AudioTask), opts ...orm.QueryOption) error {
|
||||
return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
|
||||
}
|
||||
|
||||
// Del implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) Del(ctx context.Context, model *audioencode.AudioTask, opts ...orm.QueryOption) error {
|
||||
return orm.DeleteWithContext(ctx, d.db, model, opts...)
|
||||
}
|
||||
|
||||
// EditStatus implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) EditStatus(status int, id int) error {
|
||||
if err := d.db.Model(&audioencode.AudioTask{}).Where(`id = ?`, id).Update("task_status", status).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EditStatusError implements audioencode.AudioTaskStorer.
|
||||
func (d AudioTask) EditStatusError(id, status int, s string) error {
|
||||
if err := d.db.Model(&audioencode.AudioTask{}).Where(`id = ?`, id).Updates(map[string]any{
|
||||
"task_status": status,
|
||||
"error_msg": s,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -22,6 +22,9 @@ func NewDB(db *gorm.DB) DB {
|
||||
func (d DB) AudioEncode() audioencode.AudioEncodeStorer {
|
||||
return AudioEncode(d)
|
||||
}
|
||||
func (d DB) AudioTask() audioencode.AudioTaskStorer {
|
||||
return AudioTask(d)
|
||||
}
|
||||
|
||||
// AutoMigrate sync database
|
||||
func (d DB) AutoMigrate(ok bool) DB {
|
||||
@ -30,6 +33,7 @@ func (d DB) AutoMigrate(ok bool) DB {
|
||||
}
|
||||
if err := d.db.AutoMigrate(
|
||||
new(audioencode.AudioEncode),
|
||||
new(audioencode.AudioTask),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ func NewCore(cfg *conf.Bootstrap) *Core {
|
||||
sdk.AddResponseHandler("findDevices", c.findDevicesRespH)
|
||||
sdk.AddResponseHandler("findChannels", c.findChannelsRespH)
|
||||
sdk.AddResponseHandler("getBaseConfig", c.getBaseConfigRespH)
|
||||
sdk.AddResponseHandler("findTalkUrl", c.findTalkUrlRespH)
|
||||
|
||||
config, err := c.GetBaseConfig(context.TODO(), &ConfigBaseInput{Mode: "rtsp"})
|
||||
if err != nil {
|
||||
@ -91,3 +92,7 @@ func (c Core) getBaseConfigRespH(requestID string, args json.RawMessage) (interf
|
||||
slog.Debug("Received 'getBaseConfig' from host", "request_id", requestID, "args", args)
|
||||
return nil, nil
|
||||
}
|
||||
func (c Core) findTalkUrlRespH(requestID string, args json.RawMessage) (interface{}, error) {
|
||||
slog.Debug("Received 'findTalkUrl' from host", "request_id", requestID, "args", args)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
24
internal/core/host/talk.go
Normal file
24
internal/core/host/talk.go
Normal file
@ -0,0 +1,24 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func (c Core) FindTalkUrl(ctx context.Context, in *FindTalkInput) (*FinTalkOutput, error) {
|
||||
marshal, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := c.Plugin.CallHost("findTalkUrl", marshal)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := FinTalkOutput{}
|
||||
if err = json.Unmarshal(result, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
9
internal/core/host/talk.param.go
Normal file
9
internal/core/host/talk.param.go
Normal file
@ -0,0 +1,9 @@
|
||||
package host
|
||||
|
||||
type FindTalkInput struct {
|
||||
ChannelID string `form:"channel_id"`
|
||||
}
|
||||
type FinTalkOutput struct {
|
||||
TalkUrl string `json:"talk_url"`
|
||||
Transport string `json:"transport"`
|
||||
}
|
||||
138
internal/core/transcode/code.go
Normal file
138
internal/core/transcode/code.go
Normal file
@ -0,0 +1,138 @@
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (tk *Task) StartTalkCode(wsURL string, transport string) error {
|
||||
|
||||
sampleRate := 8000
|
||||
frameSize := 160
|
||||
dir, _ := os.Getwd()
|
||||
inputFile := filepath.Join(dir, tk.EncodeUrl)
|
||||
//SSRC := tk.GetSSRCUint32()
|
||||
if _, err := os.Stat(inputFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("input file does not exist: %v", err.Error())
|
||||
}
|
||||
u, err := url.Parse(wsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid WebSocket URL: %v", err.Error())
|
||||
}
|
||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to WebSocket: %v", err.Error())
|
||||
}
|
||||
defer func() {
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
// Open G711A file
|
||||
file, err := os.Open(inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open input file: %v", err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Initialize RTP parameters
|
||||
seqNumber := uint16(0)
|
||||
// Initialize with a random timestamp base as per RTP standards
|
||||
initialTimestamp := uint32(time.Now().UnixNano() / 1e6) // Use current time as base
|
||||
|
||||
// Calculate frame duration in milliseconds
|
||||
frameDuration := time.Duration(float64(frameSize)/float64(sampleRate)*1000) * time.Millisecond
|
||||
nextSendTime := time.Now()
|
||||
|
||||
frame := make([]byte, frameSize)
|
||||
var errors error
|
||||
for {
|
||||
if tk.isEnd {
|
||||
break // End of file
|
||||
}
|
||||
n, err := file.Read(frame)
|
||||
if err != nil {
|
||||
if n == 0 {
|
||||
break // End of file
|
||||
}
|
||||
slog.Error("Error reading file", "err", err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate current timestamp based on initial + frames sent * samples per frame
|
||||
currentTimestamp := initialTimestamp + uint32(seqNumber)*uint32(frameSize)
|
||||
|
||||
// Create RTP packet for G711A
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
PayloadType: 8, // G711 A-law
|
||||
SequenceNumber: seqNumber,
|
||||
Timestamp: currentTimestamp,
|
||||
SSRC: 123456,
|
||||
},
|
||||
Payload: frame[:n],
|
||||
}
|
||||
|
||||
// Send RTP packet via WebSocket
|
||||
data, err := pkt.Marshal()
|
||||
if err != nil {
|
||||
slog.Debug("Error marshaling RTP packet", "err", err.Error())
|
||||
continue
|
||||
}
|
||||
if transport == "tcp" {
|
||||
tcpBuf := make([]byte, 4)
|
||||
tcpBuf[0] = byte(len(data) >> 24)
|
||||
tcpBuf[1] = byte(len(data) >> 16)
|
||||
tcpBuf[2] = byte(len(data) >> 8)
|
||||
tcpBuf[3] = byte(len(data))
|
||||
|
||||
err = c.WriteMessage(websocket.BinaryMessage, tcpBuf)
|
||||
if err != nil {
|
||||
errors = fmt.Errorf("error sending RTP packet via WebSocket: %v", err.Error())
|
||||
slog.Debug("Error sending RTP packet via WebSocket", "err", err.Error())
|
||||
break
|
||||
}
|
||||
} else {
|
||||
err = c.WriteMessage(websocket.BinaryMessage, data)
|
||||
if err != nil {
|
||||
errors = fmt.Errorf("error sending RTP packet via WebSocket: %v", err.Error())
|
||||
slog.Debug("Error sending RTP packet via WebSocket", "err", err.Error())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Increment sequence number
|
||||
seqNumber++
|
||||
|
||||
// Control the sending rate to match real-time
|
||||
// Wait until it's time to send the next frame
|
||||
nextSendTime = nextSendTime.Add(frameDuration)
|
||||
sleepDuration := time.Until(nextSendTime)
|
||||
if sleepDuration > 0 {
|
||||
time.Sleep(sleepDuration)
|
||||
} else {
|
||||
// If we're behind schedule, update nextSendTime to current time
|
||||
nextSendTime = time.Now().Add(frameDuration)
|
||||
}
|
||||
}
|
||||
if errors != nil {
|
||||
return errors
|
||||
}
|
||||
slog.Info("G711A file sent successfully")
|
||||
close(tk.success)
|
||||
return nil
|
||||
}
|
||||
func (tk *Task) GetSSRCUint32() uint32 {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// rand.Uint32() 会返回一个类型为 uint32 的值
|
||||
// 其范围是 0 到 2^32 - 1 (即 0 到 4294967295)
|
||||
return rand.Uint32()
|
||||
}
|
||||
@ -1,35 +1,167 @@
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"easyaudioencode/internal/core/audioencode"
|
||||
"easyaudioencode/internal/core/host"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ChannelInfo struct {
|
||||
}
|
||||
|
||||
var (
|
||||
channelsMap sync.Map
|
||||
//channelsMap = make(map[int]*ChannelInfo)
|
||||
channelsLock sync.RWMutex
|
||||
"easyaudioencode/pkg/ffmpeg"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Core struct {
|
||||
HostCore *host.Core
|
||||
AudioEncodeCore *audioencode.Core
|
||||
WorkflowCore *Workflow
|
||||
}
|
||||
|
||||
func NewCore(HostCore *host.Core, AudioEncodeCore *audioencode.Core) *Core {
|
||||
core := &Core{
|
||||
HostCore: HostCore,
|
||||
AudioEncodeCore: AudioEncodeCore,
|
||||
WorkflowCore: OpenAudioEncode(AudioEncodeCore),
|
||||
}
|
||||
|
||||
// 启用任务管理器
|
||||
return core
|
||||
}
|
||||
|
||||
func (c Core) AddAudioEncode(pullUrl, pushUrl string, id int) error {
|
||||
func OpenAudioEncode(AudioEncodeCore *audioencode.Core) *Workflow {
|
||||
wf := NewWorkflow(WorkflowConfig{
|
||||
MaxConcurrency: 100, // 并发
|
||||
CleanupInterval: 30 * time.Second, // 每30秒清理一次
|
||||
MaxTaskHistory: 500, // 最多保留500个任务历史
|
||||
RetentionTime: 60 * time.Second, // 任务保留1分钟
|
||||
})
|
||||
// 设置回调函数
|
||||
wf.SetCallbacks(
|
||||
// 完成回调
|
||||
func(task *Task) {
|
||||
id, _ := strconv.Atoi(task.ID)
|
||||
err := AudioEncodeCore.AudioTaskStatus(int(TaskCompleted), id)
|
||||
if err != nil {
|
||||
slog.Error("AudioTaskStatus", "status", TaskCompleted, "err", err.Error())
|
||||
}
|
||||
slog.Info(fmt.Sprintf("任务完成: ID=%s, Type=%s, 耗时=%v",
|
||||
task.ID, task.Type, task.endTime.Sub(task.startTime)))
|
||||
},
|
||||
// 取消回调
|
||||
func(task *Task) {
|
||||
id, _ := strconv.Atoi(task.ID)
|
||||
err := AudioEncodeCore.AudioTaskStatus(int(TaskCancelled), id)
|
||||
if err != nil {
|
||||
slog.Error("AudioTaskStatus", "status", TaskCancelled, "err", err.Error())
|
||||
}
|
||||
task.isEnd = true
|
||||
slog.Info(fmt.Sprintf("任务取消: ID=%s, Type=%s", task.ID, task.Type))
|
||||
},
|
||||
// 错误回调
|
||||
func(task *Task, errs error) {
|
||||
id, _ := strconv.Atoi(task.ID)
|
||||
err := AudioEncodeCore.AudioTaskStatusError(id, int(TaskFailed), errs.Error())
|
||||
if err != nil {
|
||||
slog.Error("AudioTaskStatus", "status", TaskFailed, "err", err.Error())
|
||||
}
|
||||
task.isEnd = true
|
||||
slog.Info(fmt.Sprintf("任务失败: ID=%s, Type=%s, 错误=%v",
|
||||
task.ID, task.Type, err))
|
||||
},
|
||||
// 清理回调
|
||||
func(task *Task, reason string) {
|
||||
slog.Info(fmt.Sprintf("任务清理: ID=%s, 原因=%s", task.ID, reason))
|
||||
},
|
||||
)
|
||||
return wf
|
||||
}
|
||||
|
||||
func (c Core) WorkflowExecute(ctx context.Context, t *Task) error {
|
||||
slog.Info(fmt.Sprintf("开始执行任务: %s (类型: %s)", t.ID, t.Type))
|
||||
if c.WorkflowCore.GetIsTasks(t.Type) {
|
||||
time.Sleep(11 * time.Second)
|
||||
}
|
||||
//return fmt.Errorf("请求失败")
|
||||
in := &host.FindTalkInput{
|
||||
ChannelID: t.Type,
|
||||
}
|
||||
info, err := c.HostCore.FindTalkUrl(ctx, in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("FindTalkUrl: %v", err.Error())
|
||||
}
|
||||
slog.Info(fmt.Sprintf("开始执行任务: URL:[%s] 类型:[%s]", info.TalkUrl, info.Transport))
|
||||
go func(in *host.FinTalkOutput) {
|
||||
t.quit <- t.StartTalkCode(in.TalkUrl, in.Transport)
|
||||
}(info)
|
||||
select {
|
||||
case errs := <-t.quit:
|
||||
slog.Info("处理异常退出", "id", t.ID)
|
||||
return errs
|
||||
case <-t.success:
|
||||
slog.Info("处理完成", "id", t.ID)
|
||||
return nil
|
||||
// 用于测试
|
||||
//case <-time.After(30 * time.Second):
|
||||
// slog.Info("任务完成", "id", t.ID)
|
||||
// t.isEnd = true
|
||||
// return nil
|
||||
case <-ctx.Done():
|
||||
slog.Error("任务中断", "id", t.ID)
|
||||
t.isEnd = true
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c Core) AddTask(id int, bid string, EncodeUrl string, duration int64) error {
|
||||
newTask := &Task{
|
||||
ID: strconv.Itoa(id),
|
||||
Type: bid,
|
||||
EncodeUrl: EncodeUrl,
|
||||
Duration: duration,
|
||||
quit: make(chan error, 3),
|
||||
success: make(chan struct{}),
|
||||
isEnd: false,
|
||||
execute: c.WorkflowExecute,
|
||||
}
|
||||
err := c.WorkflowCore.AddTask(newTask)
|
||||
if err != nil {
|
||||
slog.Error("添加任务失败", "id", id, "err", err.Error())
|
||||
return fmt.Errorf("添加任务失败 【%d】%v", id, err.Error())
|
||||
}
|
||||
slog.Info("添加任务成功", "id", id)
|
||||
return nil
|
||||
}
|
||||
func (c Core) CancelTask(id int) error {
|
||||
if c.WorkflowCore.CancelTask(strconv.Itoa(id)) {
|
||||
slog.Info("已取消任务", "id", id)
|
||||
return nil
|
||||
}
|
||||
slog.Warn("取消的任务不存在或者已被取消", "id", id)
|
||||
return fmt.Errorf("取消的任务不存在或者已被取消 %d", id)
|
||||
}
|
||||
func (c Core) GetTaskInfo(id int) (*TaskInfo, error) {
|
||||
task, t := c.WorkflowCore.GetTaskInfo(strconv.Itoa(id))
|
||||
if !t {
|
||||
slog.Debug("查询任务详情失败", "id", id)
|
||||
return &TaskInfo{}, fmt.Errorf("查询任务详情失败 %d", id)
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// StartAudioEncode 转码
|
||||
func (c Core) StartAudioEncode(inputFile, outputFile string, id int) {
|
||||
|
||||
go func() {
|
||||
status := audioencode.EncodeStatusSuccess
|
||||
err := ffmpeg.TranscodeToG711AFile(inputFile, outputFile)
|
||||
if err != nil {
|
||||
slog.Error("StartAudioEncode TranscodeToG711AFile", "err", err.Error())
|
||||
status = audioencode.EncodeStatusFailed
|
||||
}
|
||||
err = c.AudioEncodeCore.AudioEncodeStatus(status, id)
|
||||
if err != nil {
|
||||
slog.Error("StartAudioEncode AudioEncodeStatus", "err", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
680
internal/core/transcode/task.go
Normal file
680
internal/core/transcode/task.go
Normal file
@ -0,0 +1,680 @@
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task 任务定义
|
||||
type Task struct {
|
||||
ID string // 任务唯一ID
|
||||
Type string // 任务类型
|
||||
EncodeUrl string // 任务音频地址
|
||||
execute func(ctx context.Context, task *Task) error // 执行函数
|
||||
ctx context.Context // 任务上下文
|
||||
cancelFunc context.CancelFunc // 取消函数
|
||||
status TaskStatus // 任务状态
|
||||
startTime time.Time // 开始时间
|
||||
endTime time.Time // 结束时间
|
||||
createTime time.Time // 创建时间
|
||||
quit chan error
|
||||
success chan struct{}
|
||||
isEnd bool
|
||||
Duration int64
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
Status TaskStatus // 任务状态
|
||||
StartTime time.Time // 开始时间
|
||||
EndTime time.Time // 结束时间
|
||||
CreateTime time.Time // 创建时间
|
||||
}
|
||||
|
||||
// TaskStatus 任务状态
|
||||
type TaskStatus int
|
||||
|
||||
const (
|
||||
TaskPending TaskStatus = iota // 等待中
|
||||
TaskRunning // 执行中
|
||||
TaskCompleted // 已完成
|
||||
TaskCancelled // 已取消
|
||||
TaskFailed // 已失败
|
||||
)
|
||||
|
||||
// Workflow 工作流
|
||||
type Workflow struct {
|
||||
maxConcurrency int // 最大并发数
|
||||
semaphore chan struct{} // 并发信号量
|
||||
tasks map[string]*Task // 所有任务
|
||||
taskQueue map[string][]string // 按类型分组的任务队列
|
||||
runningTasks map[string]string // 正在运行的任务类型 -> 任务ID
|
||||
mu sync.RWMutex // 读写锁
|
||||
wg sync.WaitGroup // 等待组
|
||||
cleanupTicker *time.Ticker // 清理定时器
|
||||
cleanupStop chan struct{} // 清理停止信号
|
||||
|
||||
// 清理配置
|
||||
cleanupInterval time.Duration // 清理间隔
|
||||
maxTaskHistory int // 最大历史任务数
|
||||
retentionTime time.Duration // 任务保留时间
|
||||
|
||||
// 回调函数
|
||||
onComplete func(task *Task) // 完成回调
|
||||
onCancel func(task *Task) // 取消回调
|
||||
onError func(task *Task, err error) // 错误回调
|
||||
onCleanup func(task *Task, reason string) // 清理回调
|
||||
}
|
||||
|
||||
// WorkflowConfig 工作流配置
|
||||
type WorkflowConfig struct {
|
||||
MaxConcurrency int // 最大并发数,默认2
|
||||
CleanupInterval time.Duration // 清理间隔,默认30秒
|
||||
MaxTaskHistory int // 最大历史任务数,默认1000
|
||||
RetentionTime time.Duration // 任务保留时间,默认5分钟
|
||||
}
|
||||
|
||||
// NewWorkflow 创建工作流
|
||||
func NewWorkflow(config WorkflowConfig) *Workflow {
|
||||
if config.MaxConcurrency <= 0 {
|
||||
config.MaxConcurrency = 2
|
||||
}
|
||||
if config.CleanupInterval <= 0 {
|
||||
config.CleanupInterval = 30 * time.Second
|
||||
}
|
||||
if config.MaxTaskHistory <= 0 {
|
||||
config.MaxTaskHistory = 1000
|
||||
}
|
||||
if config.RetentionTime <= 0 {
|
||||
config.RetentionTime = 5 * time.Minute
|
||||
}
|
||||
|
||||
wf := &Workflow{
|
||||
maxConcurrency: config.MaxConcurrency,
|
||||
semaphore: make(chan struct{}, config.MaxConcurrency),
|
||||
tasks: make(map[string]*Task),
|
||||
taskQueue: make(map[string][]string),
|
||||
runningTasks: make(map[string]string),
|
||||
cleanupInterval: config.CleanupInterval,
|
||||
maxTaskHistory: config.MaxTaskHistory,
|
||||
retentionTime: config.RetentionTime,
|
||||
cleanupStop: make(chan struct{}),
|
||||
}
|
||||
|
||||
// 启动清理协程
|
||||
wf.startCleanupRoutine()
|
||||
|
||||
return wf
|
||||
}
|
||||
|
||||
// SetCallbacks 设置回调函数
|
||||
func (wf *Workflow) SetCallbacks(
|
||||
onComplete func(task *Task),
|
||||
onCancel func(task *Task),
|
||||
onError func(task *Task, err error),
|
||||
onCleanup func(task *Task, reason string),
|
||||
) {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
wf.onComplete = onComplete
|
||||
wf.onCancel = onCancel
|
||||
wf.onError = onError
|
||||
wf.onCleanup = onCleanup
|
||||
}
|
||||
|
||||
// AddTask 添加新任务
|
||||
func (wf *Workflow) AddTask(task *Task) error {
|
||||
if task == nil {
|
||||
return errors.New("task cannot be nil")
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
return errors.New("task ID cannot be empty")
|
||||
}
|
||||
|
||||
if task.execute == nil {
|
||||
return errors.New("task execute function cannot be nil")
|
||||
}
|
||||
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
// 检查任务是否已存在
|
||||
if _, exists := wf.tasks[task.ID]; exists {
|
||||
return fmt.Errorf("task with ID %s already exists", task.ID)
|
||||
}
|
||||
|
||||
// 设置任务初始状态
|
||||
task.status = TaskPending
|
||||
task.ctx, task.cancelFunc = context.WithCancel(context.Background())
|
||||
task.createTime = time.Now()
|
||||
|
||||
// 保存任务到map
|
||||
wf.tasks[task.ID] = task
|
||||
|
||||
// 添加到类型队列
|
||||
if _, ok := wf.taskQueue[task.Type]; !ok {
|
||||
wf.taskQueue[task.Type] = make([]string, 0)
|
||||
}
|
||||
wf.taskQueue[task.Type] = append(wf.taskQueue[task.Type], task.ID)
|
||||
|
||||
// 尝试执行任务(在锁外执行,避免死锁)
|
||||
go func() {
|
||||
wf.tryExecuteTask(task.Type)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryExecuteTask 尝试执行任务
|
||||
func (wf *Workflow) tryExecuteTask(taskType string) {
|
||||
wf.mu.Lock()
|
||||
|
||||
// 检查是否有该类型的任务正在运行
|
||||
if _, isRunning := wf.runningTasks[taskType]; isRunning {
|
||||
wf.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取该类型的任务队列
|
||||
queue, exists := wf.taskQueue[taskType]
|
||||
if !exists || len(queue) == 0 {
|
||||
wf.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取队列中的第一个任务
|
||||
taskID := queue[0]
|
||||
task, taskExists := wf.tasks[taskID]
|
||||
if !taskExists || task.status != TaskPending {
|
||||
wf.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
wf.mu.Unlock()
|
||||
|
||||
// 尝试获取并发许可(非阻塞)
|
||||
select {
|
||||
case wf.semaphore <- struct{}{}:
|
||||
// 获取成功,可以执行
|
||||
wf.executeTask(task)
|
||||
default:
|
||||
// 并发已达上限,等待下次尝试
|
||||
}
|
||||
}
|
||||
|
||||
// executeTask 执行任务
|
||||
func (wf *Workflow) executeTask(task *Task) {
|
||||
wf.mu.Lock()
|
||||
|
||||
// 再次检查任务状态
|
||||
if task.status != TaskPending {
|
||||
wf.mu.Unlock()
|
||||
<-wf.semaphore // 释放信号量
|
||||
return
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
task.status = TaskRunning
|
||||
task.startTime = time.Now()
|
||||
|
||||
// 记录该类型任务正在运行
|
||||
wf.runningTasks[task.Type] = task.ID
|
||||
|
||||
// 从队列中移除(第一个元素)
|
||||
if queue, exists := wf.taskQueue[task.Type]; exists && len(queue) > 0 {
|
||||
wf.taskQueue[task.Type] = queue[1:]
|
||||
}
|
||||
|
||||
wf.mu.Unlock()
|
||||
|
||||
wf.wg.Add(1)
|
||||
|
||||
// 启动任务执行
|
||||
go func() {
|
||||
defer func() {
|
||||
// 恢复panic
|
||||
if r := recover(); r != nil {
|
||||
err := fmt.Errorf("task panic: %v\n%s", r, debug.Stack())
|
||||
wf.handleTaskError(task, err)
|
||||
}
|
||||
|
||||
wf.taskCompleted(task.Type)
|
||||
wf.wg.Done()
|
||||
}()
|
||||
// 设置超时
|
||||
timeNum := 12 * 60 * time.Minute
|
||||
if task.Duration > 0 {
|
||||
timeNum = time.Duration(task.Duration) * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(task.ctx, timeNum)
|
||||
defer cancel()
|
||||
|
||||
// 执行任务
|
||||
err := task.execute(ctx, task)
|
||||
|
||||
// 处理任务结果
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
task.status = TaskCancelled
|
||||
task.endTime = time.Now()
|
||||
wf.triggerOnCancel(task)
|
||||
} else {
|
||||
task.status = TaskFailed
|
||||
task.endTime = time.Now()
|
||||
wf.triggerOnError(task, err)
|
||||
}
|
||||
} else {
|
||||
task.status = TaskCompleted
|
||||
task.endTime = time.Now()
|
||||
wf.triggerOnComplete(task)
|
||||
}
|
||||
|
||||
// 释放该类型的运行标记
|
||||
delete(wf.runningTasks, task.Type)
|
||||
}()
|
||||
}
|
||||
|
||||
// taskCompleted 任务完成处理
|
||||
func (wf *Workflow) taskCompleted(taskType string) {
|
||||
// 释放并发许可
|
||||
<-wf.semaphore
|
||||
|
||||
// 尝试执行下一个同类型任务
|
||||
go wf.tryExecuteTask(taskType)
|
||||
|
||||
// 检查是否有其他类型的任务可以执行
|
||||
wf.mu.RLock()
|
||||
allTypes := make([]string, 0, len(wf.taskQueue))
|
||||
for t := range wf.taskQueue {
|
||||
allTypes = append(allTypes, t)
|
||||
}
|
||||
wf.mu.RUnlock()
|
||||
|
||||
for _, t := range allTypes {
|
||||
if t != taskType {
|
||||
go wf.tryExecuteTask(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CancelTask 取消任务
|
||||
func (wf *Workflow) CancelTask(taskID string) bool {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
task, exists := wf.tasks[taskID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
switch task.status {
|
||||
case TaskPending:
|
||||
// 从队列中移除
|
||||
if queue, exists := wf.taskQueue[task.Type]; exists {
|
||||
newQueue := make([]string, 0, len(queue))
|
||||
for _, id := range queue {
|
||||
if id != taskID {
|
||||
newQueue = append(newQueue, id)
|
||||
}
|
||||
}
|
||||
wf.taskQueue[task.Type] = newQueue
|
||||
}
|
||||
|
||||
task.status = TaskCancelled
|
||||
task.endTime = time.Now()
|
||||
wf.triggerOnCancel(task)
|
||||
return true
|
||||
|
||||
case TaskRunning:
|
||||
// 取消正在执行的任务
|
||||
if task.cancelFunc != nil {
|
||||
task.cancelFunc()
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
// 已完成或已取消的任务
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupCompletedTasks 清理已完成的任务
|
||||
func (wf *Workflow) CleanupCompletedTasks() int {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
return wf.cleanupTasksInternal("manual", false)
|
||||
}
|
||||
|
||||
// CleanupOldTasks 清理旧任务(基于保留时间)
|
||||
func (wf *Workflow) CleanupOldTasks() int {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
return wf.cleanupTasksInternal("retention", false)
|
||||
}
|
||||
|
||||
// CleanupAllTasks 清理所有任务(谨慎使用)
|
||||
func (wf *Workflow) CleanupAllTasks(force bool) int {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
return wf.cleanupTasksInternal("all", force)
|
||||
}
|
||||
|
||||
// cleanupTasksInternal 内部清理方法
|
||||
func (wf *Workflow) cleanupTasksInternal(reason string, force bool) int {
|
||||
cleanedCount := 0
|
||||
now := time.Now()
|
||||
|
||||
for id, task := range wf.tasks {
|
||||
// 跳过运行中和等待中的任务(除非强制清理)
|
||||
if !force && (task.status == TaskRunning || task.status == TaskPending) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否应该清理
|
||||
shouldClean := false
|
||||
|
||||
switch reason {
|
||||
case "manual":
|
||||
// 手动清理:只清理已结束的任务
|
||||
shouldClean = task.status == TaskCompleted ||
|
||||
task.status == TaskCancelled ||
|
||||
task.status == TaskFailed
|
||||
|
||||
case "retention":
|
||||
// 基于保留时间清理
|
||||
if task.endTime.IsZero() {
|
||||
// 如果没有结束时间,使用创建时间
|
||||
shouldClean = now.Sub(task.createTime) > wf.retentionTime
|
||||
} else {
|
||||
shouldClean = now.Sub(task.endTime) > wf.retentionTime
|
||||
}
|
||||
|
||||
case "all":
|
||||
// 清理所有任务(包括运行中和等待中的)
|
||||
shouldClean = true
|
||||
|
||||
default:
|
||||
// 默认清理已结束的任务
|
||||
shouldClean = task.status == TaskCompleted ||
|
||||
task.status == TaskCancelled ||
|
||||
task.status == TaskFailed
|
||||
}
|
||||
|
||||
if shouldClean {
|
||||
// 触发清理回调
|
||||
if wf.onCleanup != nil {
|
||||
wf.onCleanup(task, reason)
|
||||
}
|
||||
|
||||
// 从tasks map中删除
|
||||
delete(wf.tasks, id)
|
||||
cleanedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空的任务队列
|
||||
for taskType, queue := range wf.taskQueue {
|
||||
newQueue := make([]string, 0, len(queue))
|
||||
for _, taskID := range queue {
|
||||
if _, exists := wf.tasks[taskID]; exists {
|
||||
newQueue = append(newQueue, taskID)
|
||||
}
|
||||
}
|
||||
wf.taskQueue[taskType] = newQueue
|
||||
|
||||
// 如果队列为空,删除该类型
|
||||
if len(newQueue) == 0 {
|
||||
delete(wf.taskQueue, taskType)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理运行任务记录
|
||||
for taskType, taskID := range wf.runningTasks {
|
||||
if _, exists := wf.tasks[taskID]; !exists {
|
||||
delete(wf.runningTasks, taskType)
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
// startCleanupRoutine 启动清理协程
|
||||
func (wf *Workflow) startCleanupRoutine() {
|
||||
wf.cleanupTicker = time.NewTicker(wf.cleanupInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-wf.cleanupTicker.C:
|
||||
// 定期清理旧任务
|
||||
wf.mu.Lock()
|
||||
cleaned := wf.cleanupTasksInternal("retention", false)
|
||||
wf.mu.Unlock()
|
||||
|
||||
if cleaned > 0 {
|
||||
log.Printf("自动清理了 %d 个旧任务", cleaned)
|
||||
}
|
||||
|
||||
// 检查任务数量,如果超过限制,清理最旧的任务
|
||||
wf.mu.Lock()
|
||||
if len(wf.tasks) > wf.maxTaskHistory {
|
||||
wf.cleanupExcessTasks()
|
||||
}
|
||||
wf.mu.Unlock()
|
||||
|
||||
case <-wf.cleanupStop:
|
||||
wf.cleanupTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// cleanupExcessTasks 清理超出限制的任务
|
||||
func (wf *Workflow) cleanupExcessTasks() {
|
||||
if len(wf.tasks) <= wf.maxTaskHistory {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有任务并按创建时间排序
|
||||
tasks := make([]*Task, 0, len(wf.tasks))
|
||||
for _, task := range wf.tasks {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
// 按创建时间排序(最旧的在前面)
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
for j := i + 1; j < len(tasks); j++ {
|
||||
if tasks[j].createTime.Before(tasks[i].createTime) {
|
||||
tasks[i], tasks[j] = tasks[j], tasks[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算需要清理的数量
|
||||
excess := len(tasks) - wf.maxTaskHistory
|
||||
|
||||
// 清理最旧的任务(跳过运行中和等待中的任务)
|
||||
cleaned := 0
|
||||
for i := 0; i < len(tasks) && cleaned < excess; i++ {
|
||||
task := tasks[i]
|
||||
|
||||
// 只清理已结束的任务
|
||||
if task.status == TaskCompleted || task.status == TaskCancelled || task.status == TaskFailed {
|
||||
if wf.onCleanup != nil {
|
||||
wf.onCleanup(task, "max_history")
|
||||
}
|
||||
|
||||
delete(wf.tasks, task.ID)
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
|
||||
if cleaned > 0 {
|
||||
log.Printf("清理了 %d 个任务以保持历史记录不超过 %d", cleaned, wf.maxTaskHistory)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTaskStatus 获取任务状态
|
||||
func (wf *Workflow) GetTaskStatus(taskID string) (TaskStatus, bool) {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
task, exists := wf.tasks[taskID]
|
||||
if !exists {
|
||||
return TaskPending, false
|
||||
}
|
||||
|
||||
return task.status, true
|
||||
}
|
||||
|
||||
// GetTaskInfo 获取任务详情
|
||||
func (wf *Workflow) GetTaskInfo(taskID string) (*TaskInfo, bool) {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
task, exists := wf.tasks[taskID]
|
||||
if !exists {
|
||||
return &TaskInfo{}, false
|
||||
}
|
||||
return &TaskInfo{
|
||||
Status: task.status,
|
||||
EndTime: task.endTime,
|
||||
CreateTime: task.createTime,
|
||||
StartTime: task.startTime,
|
||||
}, true
|
||||
}
|
||||
|
||||
// GetRunningTasks 获取正在运行的任务
|
||||
func (wf *Workflow) GetRunningTasks() []*Task {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
result := make([]*Task, 0)
|
||||
for _, task := range wf.tasks {
|
||||
if task.status == TaskRunning {
|
||||
result = append(result, task)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPendingTasks 获取等待中的任务
|
||||
func (wf *Workflow) GetPendingTasks() []*Task {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
result := make([]*Task, 0)
|
||||
for _, task := range wf.tasks {
|
||||
if task.status == TaskPending {
|
||||
result = append(result, task)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetPendingTasks 获取等待中的任务
|
||||
func (wf *Workflow) GetIsTasks(bid string) bool {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
result := false
|
||||
for _, task := range wf.tasks {
|
||||
if task.Type == bid && task.status != TaskPending && task.status != TaskRunning {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTaskCount 获取各种状态的任务数量
|
||||
func (wf *Workflow) GetTaskCount() (total, running, pending, completed, cancelled, failed int) {
|
||||
wf.mu.RLock()
|
||||
defer wf.mu.RUnlock()
|
||||
|
||||
total = len(wf.tasks)
|
||||
for _, task := range wf.tasks {
|
||||
switch task.status {
|
||||
case TaskRunning:
|
||||
running++
|
||||
case TaskPending:
|
||||
pending++
|
||||
case TaskCompleted:
|
||||
completed++
|
||||
case TaskCancelled:
|
||||
cancelled++
|
||||
case TaskFailed:
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Wait 等待所有任务完成
|
||||
func (wf *Workflow) Wait() {
|
||||
wf.wg.Wait()
|
||||
}
|
||||
|
||||
// Stop 停止工作流
|
||||
func (wf *Workflow) Stop() {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
// 发送清理停止信号
|
||||
close(wf.cleanupStop)
|
||||
|
||||
// 取消所有任务
|
||||
for _, task := range wf.tasks {
|
||||
if task.status == TaskPending || task.status == TaskRunning {
|
||||
if task.cancelFunc != nil {
|
||||
task.cancelFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发回调函数
|
||||
func (wf *Workflow) triggerOnComplete(task *Task) {
|
||||
if wf.onComplete != nil {
|
||||
wf.onComplete(task)
|
||||
}
|
||||
}
|
||||
|
||||
func (wf *Workflow) triggerOnCancel(task *Task) {
|
||||
if wf.onCancel != nil {
|
||||
wf.onCancel(task)
|
||||
}
|
||||
}
|
||||
|
||||
func (wf *Workflow) triggerOnError(task *Task, err error) {
|
||||
if wf.onError != nil {
|
||||
wf.onError(task, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (wf *Workflow) handleTaskError(task *Task, err error) {
|
||||
wf.mu.Lock()
|
||||
defer wf.mu.Unlock()
|
||||
|
||||
task.status = TaskFailed
|
||||
task.endTime = time.Now()
|
||||
wf.triggerOnError(task, err)
|
||||
delete(wf.runningTasks, task.Type)
|
||||
}
|
||||
@ -75,6 +75,7 @@ func setupRouter(r *gin.Engine, uc *Usecase) {
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, target)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "/extensions/easyaudioencode") {
|
||||
// 改为前缀替换并在当前请求内重新分发,而不是重定向
|
||||
newPath := strings.TrimPrefix(p, "/extensions/easyaudioencode")
|
||||
|
||||
@ -11,8 +11,8 @@ import (
|
||||
"git.lnton.com/lnton/pkg/reason"
|
||||
"git.lnton.com/lnton/pkg/web"
|
||||
"github.com/gin-gonic/gin"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Loger interface {
|
||||
@ -41,60 +41,30 @@ func RegisterAudioEncode(g gin.IRouter, api AudioEncodeAPI, handler ...gin.Handl
|
||||
group.DELETE("/:id", web.WarpH(api.delAudioEncode))
|
||||
|
||||
group.POST("/upload", web.WarpH(api.uploadAudioHandler))
|
||||
group.GET("/:id/start", web.WarpH(api.getStart))
|
||||
group.GET("/:id/stop", web.WarpH(api.getStop))
|
||||
group.GET("/:id/snap", web.WarpH(api.getSnap))
|
||||
group.GET("/:id/code", web.WarpH(api.getCode))
|
||||
|
||||
// 任务
|
||||
groupTask := g.Group("/api/audiotask", handler...)
|
||||
groupTask.POST("", web.WarpH(api.addAudioTask))
|
||||
groupTask.GET("", web.WarpH(api.findAudioTask))
|
||||
groupTask.DELETE("/:id", web.WarpH(api.delAudioTask))
|
||||
groupTask.DELETE("", web.WarpH(api.delAudioTaskAll))
|
||||
groupTask.GET("/cancel/:id", web.WarpH(api.cancelAudioTask))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// >>> pulltopushServer >>>>>>>>>>>>>>>>>>>>
|
||||
|
||||
func (a AudioEncodeAPI) getStart(c *gin.Context, _ *struct{}) (any, error) {
|
||||
// >>> transcodeCore >>>>>>>>>>>>>>>>>>>>
|
||||
func (a AudioEncodeAPI) getCode(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
|
||||
info, err := a.core.GetAudioEncode(c.Request.Context(), ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
//err = a.sourceCore.StartStream(info.ID)
|
||||
//if err != nil {
|
||||
// return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`StartStream [%d] err [%s]`, ID, err.Error()))
|
||||
//}
|
||||
err = a.core.AudioEncodeEnable(true, ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`AudioEncodeEnable [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
a.transcodeCore.StartAudioEncode(info.SourceUrl, info.EncodeUrl, info.ID)
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) getStop(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
//err = a.sourceCore.StopStream(info.ID)
|
||||
//if err != nil {
|
||||
// return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`StopStream [%d] err [%s]`, ID, err.Error()))
|
||||
//}
|
||||
err = a.core.AudioEncodeEnable(false, ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`AudioEncodeEnable [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) getSnap(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
//_ = a.sourceCore.SnapStream(info.ID)
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
return gin.H{"data": fmt.Sprintf("/snap_%d.jpg", ID)}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) findAudioEncode(c *gin.Context, in *audioencode.FindAudioEncodeInput) (any, error) {
|
||||
items, total, err := a.core.FindAudioEncode(c.Request.Context(), in)
|
||||
rows := make([]map[string]interface{}, 0)
|
||||
@ -112,6 +82,7 @@ func (a AudioEncodeAPI) findAudioEncode(c *gin.Context, in *audioencode.FindAudi
|
||||
row["created_at"] = item.CreatedAt
|
||||
row["updated_at"] = item.UpdatedAt
|
||||
row["encode_status"] = item.EncodeStatus
|
||||
row["duration"] = item.Duration
|
||||
row["des"] = item.Des
|
||||
rows = append(rows, row)
|
||||
}
|
||||
@ -136,7 +107,7 @@ func (a AudioEncodeAPI) getAudioEncode(c *gin.Context, _ *struct{}) (any, error)
|
||||
row["created_at"] = item.CreatedAt
|
||||
row["updated_at"] = item.UpdatedAt
|
||||
row["encode_status"] = item.EncodeStatus
|
||||
|
||||
row["duration"] = item.Duration
|
||||
row["des"] = item.Des
|
||||
return gin.H{"data": row}, err
|
||||
}
|
||||
@ -167,9 +138,18 @@ func (a AudioEncodeAPI) addAudioEncode(c *gin.Context, in *audioencode.AddAudioE
|
||||
|
||||
func (a AudioEncodeAPI) delAudioEncode(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
_, err := a.core.DelAudioEncode(c.Request.Context(), ID)
|
||||
info, err := a.core.DelAudioEncode(c.Request.Context(), ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
|
||||
err = os.Remove(info.SourceUrl)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del SourceUrl [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
err = os.Remove(info.EncodeUrl)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del SourceUrl [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
|
||||
131
internal/web/api/audiotask.go
Normal file
131
internal/web/api/audiotask.go
Normal file
@ -0,0 +1,131 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"easyaudioencode/internal/core/audioencode"
|
||||
"fmt"
|
||||
"git.lnton.com/lnton/pkg/reason"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (a AudioEncodeAPI) findAudioTask(c *gin.Context, in *audioencode.FindAudioTaskInput) (any, error) {
|
||||
items, total, err := a.core.FindAudioTask(c.Request.Context(), in)
|
||||
rows := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, item := range items {
|
||||
//row := structs.Map(item)
|
||||
row := make(map[string]interface{})
|
||||
|
||||
row["audio_id"] = item.AudioID
|
||||
row["audio_name"] = item.AudioName
|
||||
row["channel_id"] = item.ChannelID
|
||||
row["channel_name"] = item.ChannelName
|
||||
row["size"] = item.Size
|
||||
row["duration"] = item.Duration
|
||||
row["id"] = item.ID
|
||||
row["mode"] = item.Mode
|
||||
row["created_at"] = item.CreatedAt
|
||||
row["updated_at"] = item.UpdatedAt
|
||||
row["task_status"] = item.TaskStatus
|
||||
row["error_msg"] = item.ErrorMsg
|
||||
task, errs := a.transcodeCore.GetTaskInfo(item.ID)
|
||||
if errs == nil {
|
||||
row["task_status"] = task.Status
|
||||
row["end_time"] = task.EndTime
|
||||
row["create_time"] = task.CreateTime
|
||||
row["start_time"] = task.StartTime
|
||||
} else {
|
||||
if item.TaskStatus == 1 || item.TaskStatus == 0 {
|
||||
row["task_status"] = 2
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
return gin.H{"items": rows, "total": total}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) addAudioTask(c *gin.Context, in *audioencode.AddAudioTaskInput) (any, error) {
|
||||
if len(in.Items) == 0 {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add audioencode Items is empty`))
|
||||
}
|
||||
var outputList []audioencode.AddErrorOutput
|
||||
for _, item := range in.Items {
|
||||
audioInfo, err := a.core.GetAudioEncode(c.Request.Context(), item.AudioID)
|
||||
if err != nil {
|
||||
list := audioencode.AddErrorOutput{
|
||||
AudioID: item.AudioID,
|
||||
ChannelID: item.ChannelID,
|
||||
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
|
||||
ChannelName: item.ChannelName,
|
||||
}
|
||||
outputList = append(outputList, list)
|
||||
continue
|
||||
}
|
||||
inAdd := &audioencode.AddAudioTask{
|
||||
AudioID: item.AudioID,
|
||||
ChannelID: item.ChannelID,
|
||||
ChannelName: item.ChannelName,
|
||||
Size: audioInfo.Size,
|
||||
Duration: audioInfo.Duration,
|
||||
AudioName: audioInfo.Name,
|
||||
Mode: audioInfo.Mode,
|
||||
}
|
||||
info, err := a.core.AddAudioTask(c.Request.Context(), inAdd)
|
||||
if err != nil {
|
||||
list := audioencode.AddErrorOutput{
|
||||
AudioID: item.AudioID,
|
||||
ChannelID: item.ChannelID,
|
||||
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
|
||||
ChannelName: item.ChannelName,
|
||||
}
|
||||
outputList = append(outputList, list)
|
||||
continue
|
||||
}
|
||||
|
||||
err = a.transcodeCore.AddTask(info.ID, info.ChannelID, audioInfo.EncodeUrl, audioInfo.Duration)
|
||||
if err != nil {
|
||||
list := audioencode.AddErrorOutput{
|
||||
AudioID: item.AudioID,
|
||||
ChannelID: item.ChannelID,
|
||||
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
|
||||
ChannelName: item.ChannelName,
|
||||
}
|
||||
outputList = append(outputList, list)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(outputList) == 0 {
|
||||
return gin.H{"data": "ok", "msg": "下发成功"}, nil
|
||||
}
|
||||
return gin.H{"data": outputList, "msg": "下发失败通道!"}, nil
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) cancelAudioTask(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
err := a.transcodeCore.CancelTask(ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add CancelTask err [%s]`, err.Error()))
|
||||
}
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) delAudioTask(c *gin.Context, _ *struct{}) (any, error) {
|
||||
ID, _ := strconv.Atoi(c.Param("id"))
|
||||
_, err := a.core.DelAudioTask(c.Request.Context(), ID)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, ID, err.Error()))
|
||||
}
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
|
||||
func (a AudioEncodeAPI) delAudioTaskAll(c *gin.Context, in *audioencode.DelAudioTaskInput) (any, error) {
|
||||
if len(in.IDs) == 0 {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode ids is empty`))
|
||||
}
|
||||
_, err := a.core.DelAudioTaskAll(c.Request.Context(), in.IDs)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, in.IDs, err.Error()))
|
||||
}
|
||||
return gin.H{"data": "OK!"}, err
|
||||
}
|
||||
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"easyaudioencode/internal/core/audioencode"
|
||||
"easyaudioencode/pkg/ffmpeg"
|
||||
"fmt"
|
||||
"git.lnton.com/lnton/pkg/reason"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -44,6 +45,9 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
|
||||
if err := os.MkdirAll(filepath.Join(uploadDir, sourceDir), 0755); err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建上传目录失败: %v", err.Error()))
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(uploadDir, encodeDir), 0755); err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建转码目录失败: %v", err.Error()))
|
||||
}
|
||||
// 限制请求体大小
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxUploadSize)
|
||||
if err := c.Request.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
@ -76,7 +80,7 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
|
||||
// 生成唯一文件名(避免覆盖)
|
||||
fileName := fmt.Sprintf("%s%s", uuidStr, fileExt)
|
||||
filePath := filepath.Join(uploadDir, sourceDir, fileName)
|
||||
|
||||
outputFile := filepath.Join(uploadDir, encodeDir, fmt.Sprintf("%s.g711a", uuidStr))
|
||||
// 创建目标文件
|
||||
dstFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
@ -88,18 +92,25 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
|
||||
if _, err := io.Copy(dstFile, file); err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("保存文件失败: %v", err.Error()))
|
||||
}
|
||||
|
||||
in := &audioencode.AddAudioEncodeInput{
|
||||
Name: sourceFileName,
|
||||
FileName: fileName,
|
||||
Size: fileHeader.Size,
|
||||
SourceUrl: filePath,
|
||||
Mode: fileExt,
|
||||
Name: sourceFileName,
|
||||
FileName: fileName,
|
||||
Size: fileHeader.Size,
|
||||
SourceUrl: filePath,
|
||||
EncodeUrl: outputFile,
|
||||
Mode: fileExt,
|
||||
EncodeStatus: audioencode.EncodeStatusPing,
|
||||
}
|
||||
_, err = a.core.AddAudioEncode(c.Request.Context(), in)
|
||||
duration, _, err := ffmpeg.GetAudioDuration(filePath)
|
||||
if err == nil {
|
||||
in.Duration = int64(duration.Seconds())
|
||||
}
|
||||
info, err := a.core.AddAudioEncode(c.Request.Context(), in)
|
||||
if err != nil {
|
||||
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add audioencode err [%s]`, err.Error()))
|
||||
}
|
||||
|
||||
a.transcodeCore.StartAudioEncode(filePath, outputFile, info.ID)
|
||||
return gin.H{"data": "上传成功!", "filePath": filePath, "filename": fileName, "size": fileHeader.Size}, err
|
||||
}
|
||||
|
||||
|
||||
197
pkg/ffmpeg/core.go
Normal file
197
pkg/ffmpeg/core.go
Normal file
@ -0,0 +1,197 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tcolgate/mp3"
|
||||
"github.com/youpy/go-wav"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 音频转码配置
|
||||
type TranscodeConfig struct {
|
||||
InputPath string // 输入文件路径(MP3/WAV)
|
||||
OutputPath string // 输出文件路径(G711A)
|
||||
SampleRate int // 采样率(默认 8000 Hz,G711A 标准采样率)
|
||||
Channels int // 声道数(默认 1,单声道)
|
||||
}
|
||||
|
||||
// 默认转码配置
|
||||
func defaultTranscodeConfig(input, output string) TranscodeConfig {
|
||||
return TranscodeConfig{
|
||||
InputPath: input,
|
||||
OutputPath: output,
|
||||
SampleRate: 8000, // G711A 标准采样率
|
||||
Channels: 1, // 单声道(电话/语音常用)
|
||||
}
|
||||
}
|
||||
|
||||
// 校验输入文件类型(仅允许 MP3/WAV)
|
||||
func validateInputFile(inputPath string) error {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("输入文件不存在: %s", inputPath)
|
||||
}
|
||||
|
||||
// 校验文件扩展名
|
||||
ext := strings.ToLower(filepath.Ext(inputPath))
|
||||
if ext != ".mp3" && ext != ".wav" {
|
||||
return fmt.Errorf("仅支持 MP3/WAV 格式,当前文件扩展名: %s", ext)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MP3/WAV 转 G711A(PCMA)
|
||||
func TranscodeToG711A(config TranscodeConfig) error {
|
||||
// 1. 校验输入文件
|
||||
if err := validateInputFile(config.InputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 补全默认配置
|
||||
if config.SampleRate <= 0 {
|
||||
config.SampleRate = 8000
|
||||
}
|
||||
if config.Channels <= 0 {
|
||||
config.Channels = 1
|
||||
}
|
||||
|
||||
// 3. 确保输出目录存在
|
||||
outputDir := filepath.Dir(config.OutputPath)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 4. 构建 FFmpeg 命令
|
||||
// 核心参数说明:
|
||||
// -i: 输入文件
|
||||
// -ar: 采样率
|
||||
// -ac: 声道数
|
||||
// -f: 输出格式(alaw 即 G711A)
|
||||
// -y: 覆盖已存在的输出文件
|
||||
cmdArgs := []string{
|
||||
"-i", config.InputPath,
|
||||
"-ar", fmt.Sprintf("%d", config.SampleRate),
|
||||
"-ac", fmt.Sprintf("%d", config.Channels),
|
||||
"-f", "alaw", // 指定输出格式为 G711A(PCMA)
|
||||
"-y", // 覆盖输出文件(无需确认)
|
||||
config.OutputPath,
|
||||
}
|
||||
dir, _ := os.Getwd() // Windows 路径用反斜杠,或双正斜杠
|
||||
ffmpegPath := ""
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
ffmpegPath = filepath.Join(dir, "ffmpeg")
|
||||
case "windows":
|
||||
ffmpegPath = filepath.Join(dir, "ffmpeg.exe")
|
||||
}
|
||||
|
||||
// 执行 FFmpeg 命令
|
||||
cmd := exec.Command(ffmpegPath, cmdArgs...)
|
||||
// 捕获 FFmpeg 输出(便于调试)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("转码失败: %v, FFmpeg 输出: %s", err, string(output))
|
||||
}
|
||||
|
||||
// 校验输出文件是否生成
|
||||
if _, err := os.Stat(config.OutputPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("转码后输出文件未生成: %s", config.OutputPath)
|
||||
}
|
||||
|
||||
fmt.Printf("转码成功!输入: %s → 输出: %s\n", config.InputPath, config.OutputPath)
|
||||
return nil
|
||||
}
|
||||
func TranscodeToG711AFile(inputFile, outputFile string) error {
|
||||
// 也可自定义配置(比如调整采样率)
|
||||
/*
|
||||
customConfig := TranscodeConfig{
|
||||
InputPath: "./uploads/test.wav",
|
||||
OutputPath: "./uploads/test_custom.g711a",
|
||||
SampleRate: 16000, // 自定义采样率
|
||||
Channels: 1,
|
||||
}
|
||||
err = TranscodeToG711A(customConfig)
|
||||
*/
|
||||
// 使用默认配置转码
|
||||
err := TranscodeToG711A(defaultTranscodeConfig(inputFile, outputFile))
|
||||
if err != nil {
|
||||
fmt.Printf("转码失败: %v\n", err)
|
||||
return fmt.Errorf("转码失败: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// GetMP3Duration 获取MP3文件时长
|
||||
func GetMP3Duration(filePath string) (time.Duration, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
decoder := mp3.NewDecoder(file)
|
||||
var frame mp3.Frame
|
||||
var totalDuration float64
|
||||
skipped := 0
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(&frame, &skipped); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
totalDuration += frame.Duration().Seconds()
|
||||
}
|
||||
|
||||
duration := time.Duration(totalDuration * float64(time.Second))
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// GetWAVDurationOptimized 优化的WAV文件时长获取方法
|
||||
func GetWAVDurationOptimized(filePath string) (time.Duration, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := wav.NewReader(file)
|
||||
|
||||
// 使用库提供的Duration方法
|
||||
duration, err := reader.Duration()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// GetAudioDuration 获取音频文件时长(支持MP3和WAV)
|
||||
func GetAudioDuration(filePath string) (time.Duration, string, error) {
|
||||
// 根据文件扩展名判断文件类型
|
||||
if len(filePath) > 4 {
|
||||
ext := filePath[len(filePath)-4:]
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
duration, err := GetMP3Duration(filePath)
|
||||
return duration, "MP3", err
|
||||
case ".wav":
|
||||
duration, err := GetWAVDurationOptimized(filePath)
|
||||
return duration, "WAV", err
|
||||
default:
|
||||
return 0, "", fmt.Errorf("不支持的音频格式: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法从扩展名判断,可以尝试根据文件头部信息判断
|
||||
return 0, "", fmt.Errorf("无法识别的音频格式")
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,2 +1,3 @@
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_WEB_BASE_URL=/
|
||||
VITE_WEB_BASE_URL=/
|
||||
VITE_AUDIO_BASE_URL=/
|
||||
@ -1,2 +1,3 @@
|
||||
VITE_API_BASE_URL=/extensions/easyaudioencode/api
|
||||
VITE_WEB_BASE_URL=/extensions/easyaudioencode/web
|
||||
VITE_WEB_BASE_URL=/extensions/easyaudioencode/web
|
||||
VITE_AUDIO_BASE_URL=/extensions/easyaudioencode/
|
||||
@ -10,7 +10,7 @@ export async function GetAudioEncode(data: EncodeReq) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
* 创建
|
||||
* @param data 创建参数
|
||||
*/
|
||||
export async function CreateAudioEncode(data: CreateAudioEncodeReq) {
|
||||
@ -19,39 +19,23 @@ export async function CreateAudioEncode(data: CreateAudioEncodeReq) {
|
||||
|
||||
|
||||
/**
|
||||
* 开启推流
|
||||
* @param id 任务ID
|
||||
* 重新转码
|
||||
* @param id ID
|
||||
*/
|
||||
export async function GetAudioEncodeStart(id: number) {
|
||||
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/start`);
|
||||
export async function GetAudioEncodeCode(id: number) {
|
||||
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/code`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭推流
|
||||
* @param id 任务ID
|
||||
*/
|
||||
export async function GetAudioEncodeStop(id: number) {
|
||||
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/stop`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快照
|
||||
* @param id 任务ID
|
||||
*/
|
||||
export async function GetAudioEncodeSnap(id: number) {
|
||||
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/snap`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务详情
|
||||
* @param id 任务ID
|
||||
* 获取详情
|
||||
* @param id ID
|
||||
*/
|
||||
export async function GetAudioEncodeById(id: string) {
|
||||
return await GET<AudioEncodeDetailRes>(`/audioencode/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务
|
||||
* 更新
|
||||
* @param data 更新参数(需包含 id)
|
||||
*/
|
||||
export async function UpdateAudioEncode(data: UpdateAudioEncodeReq) {
|
||||
@ -60,8 +44,8 @@ export async function UpdateAudioEncode(data: UpdateAudioEncodeReq) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
* @param id 任务ID
|
||||
* 删除
|
||||
* @param id ID
|
||||
*/
|
||||
export async function DeleteAudioEncode(id: number) {
|
||||
return await DELETE<AudioEncodeBaseRes>(`/audioencode/${id}`);
|
||||
|
||||
@ -7,7 +7,7 @@ const codeMessage: { [key: number]: string } = {
|
||||
201: "新建或修改数据成功。",
|
||||
202: "一个请求已经进入后台排队(异步任务)。",
|
||||
204: "删除数据成功。",
|
||||
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
|
||||
// 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
|
||||
401: "用户没有权限(令牌、用户名、密码错误)。",
|
||||
403: "用户得到授权,但是访问是被禁止的。",
|
||||
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
|
||||
|
||||
44
web/src/api/task.ts
Normal file
44
web/src/api/task.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { GET, POST, PUT, DELETE } from "./http";
|
||||
import type { AudioTaskBaseRes, AudioTaskRes, CreateAudioTaskReq, TaskReq, DelAudioTaskReq } from "../types/audiotask";
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @returns 文件列表
|
||||
*/
|
||||
export async function GetAudioTask(data: TaskReq) {
|
||||
return await GET<AudioTaskRes>(`/audiotask`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建
|
||||
* @param data 创建参数
|
||||
*/
|
||||
export async function CreateAudioTask(data: CreateAudioTaskReq) {
|
||||
return await POST<AudioTaskBaseRes>(`/audiotask`, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消
|
||||
* @param id ID
|
||||
*/
|
||||
export async function GetAudioTaskCancel(id: number) {
|
||||
return await GET<AudioTaskBaseRes>(`/audiotask/cancel/${id}`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param id ID
|
||||
*/
|
||||
export async function DeleteAudioTask(id: number) {
|
||||
return await DELETE<AudioTaskBaseRes>(`/audiotask/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
* @param ids ID
|
||||
*/
|
||||
export async function DeleteAudioTaskAll(data: DelAudioTaskReq) {
|
||||
return await DELETE<AudioTaskBaseRes>(`/audiotask`, data);
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import { useRef, useState, useMemo } from "react";
|
||||
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip } from "antd";
|
||||
import { PlayCircleOutlined, EditOutlined, NotificationOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip, notification } from "antd";
|
||||
import { PlayCircleOutlined, EditOutlined, NotificationOutlined, DeleteOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { GetAudioEncode, DeleteAudioEncode, GetAudioEncodeStart, GetAudioEncodeStop } from "../api/audio";
|
||||
import { GetAudioEncode, DeleteAudioEncode, GetAudioEncodeCode } from "../api/audio";
|
||||
import { CreateAudioTask } from "../api/task";
|
||||
import type { AudioEncodeItem } from "../types/audioencode";
|
||||
import type { AddTaskItem } from "../types/audiotask";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import AddAudioEncode, { AddAudioEncodeRef } from "./AddAudioEncode";
|
||||
import PlayerModal, { PlayerModalRef } from "./PlayerModal";
|
||||
@ -11,8 +13,8 @@ import ChannelModel, { IChannelModelFunc } from "./channel/Channel";
|
||||
import UploadAudio from './UploadAudio';
|
||||
import { useGlobal } from "../Context";
|
||||
import { FormatFileSizeToString } from "../utils/rate";
|
||||
import { formatSecondsToHMS } from "../utils/time";
|
||||
import Filter from "./Filter";
|
||||
|
||||
export default function AudioEncodePage() {
|
||||
const { ErrorHandle } = useGlobal();
|
||||
const dialogRef = useRef<AddAudioEncodeRef>(null);
|
||||
@ -59,32 +61,42 @@ export default function AudioEncodePage() {
|
||||
ErrorHandle(error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const { mutate: startMutation } = useMutation({
|
||||
mutationFn: GetAudioEncodeStart,
|
||||
// 转码任务
|
||||
const [codeLoadings, setCodeLoadings] = useState<number[]>([]);
|
||||
const { mutate: codeMutation } = useMutation({
|
||||
mutationFn: GetAudioEncodeCode,
|
||||
onMutate: (id: number) => {
|
||||
setDelLoadings((prev) => [...prev, id]);
|
||||
},
|
||||
onSuccess: (_, ctx) => {
|
||||
message.success("开启成功");
|
||||
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
|
||||
message.success("转码成功");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: Error, ctx) => {
|
||||
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
|
||||
ErrorHandle(error);
|
||||
},
|
||||
});
|
||||
const { mutate: stopMutation } = useMutation({
|
||||
mutationFn: GetAudioEncodeStop,
|
||||
onMutate: (id: number) => {
|
||||
},
|
||||
onSuccess: (_, ctx) => {
|
||||
message.success("关闭成功");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: Error, ctx) => {
|
||||
ErrorHandle(error);
|
||||
|
||||
const { mutate: createTaskMutate, isPending: creating } = useMutation({
|
||||
mutationFn: CreateAudioTask,
|
||||
onSuccess: (v, ctx) => {
|
||||
if (v.data.data == 'ok') {
|
||||
message.success("下发任务成功");
|
||||
} else {
|
||||
notification.error({
|
||||
message: '下发任务失败,可以重新下发!',
|
||||
description:
|
||||
JSON.stringify(v.data.data),
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
onError: ErrorHandle,
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
// 打开新增模态框
|
||||
const handleAdd = () => {
|
||||
dialogRef.current?.open();
|
||||
@ -95,7 +107,7 @@ export default function AudioEncodePage() {
|
||||
dialogRef.current?.open(disk);
|
||||
};
|
||||
const handleNotification = (disk: AudioEncodeItem) => {
|
||||
channelRef.current?.openModal(disk.id)
|
||||
channelRef.current?.openModal(disk.id, disk.name)
|
||||
}
|
||||
const handlePlay = (disk: AudioEncodeItem) => {
|
||||
playerModalRef.current?.open(disk);
|
||||
@ -134,7 +146,16 @@ export default function AudioEncodePage() {
|
||||
dataIndex: "name",
|
||||
align: "center",
|
||||
},
|
||||
|
||||
{
|
||||
title: "文件名称",
|
||||
dataIndex: "file_name",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{text}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "mode",
|
||||
@ -156,16 +177,18 @@ export default function AudioEncodePage() {
|
||||
),
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
title: "文件名称",
|
||||
dataIndex: "file_name",
|
||||
title: "时长",
|
||||
dataIndex: "duration",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{text}
|
||||
{formatSecondsToHMS(text)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "encode_status",
|
||||
@ -175,7 +198,16 @@ export default function AudioEncodePage() {
|
||||
{text == 0 && <Tag color="#999">未转码</Tag>}
|
||||
{text == 1 && <Tag color="#87d068">转码成功</Tag>}
|
||||
{text == 2 && <Tag color="#87d068">转码中</Tag>}
|
||||
{text == 3 && <Tag color="#f50">转码失败</Tag>}
|
||||
{text == 3 && <>
|
||||
<Tag color="#f50">转码失败</Tag>
|
||||
<Tooltip placement="top" title="重新转码" color="#fff">
|
||||
<Button
|
||||
onClick={() => codeMutation(record.id)}
|
||||
loading={codeLoadings.includes(record.id)}
|
||||
icon={<ReloadOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@ -297,8 +329,8 @@ export default function AudioEncodePage() {
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
{/* 广播模态框 */}
|
||||
<ChannelModel ref={channelRef} onCallback={(id: any, bid: any) => {
|
||||
console.log(id, bid);
|
||||
<ChannelModel ref={channelRef} onCallback={(item: AddTaskItem[]) => {
|
||||
createTaskMutate({ items: item })
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
|
||||
303
web/src/components/AudioTask.tsx
Normal file
303
web/src/components/AudioTask.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import { useRef, useState, useMemo } from "react";
|
||||
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip } from "antd";
|
||||
import { CloseOutlined, DeleteOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { GetAudioTask, DeleteAudioTask, GetAudioTaskCancel, DeleteAudioTaskAll } from "../api/task";
|
||||
import type { AudioTaskItem } from "../types/audiotask";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import UploadAudio from './UploadAudio';
|
||||
import { useGlobal } from "../Context";
|
||||
import { FormatFileSizeToString } from "../utils/rate";
|
||||
import { formatSecondsToHMS } from "../utils/time";
|
||||
import Filter from "./Filter";
|
||||
|
||||
export default function AudioTaskPage() {
|
||||
const { ErrorHandle } = useGlobal();
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
size: 10,
|
||||
name: ""
|
||||
});
|
||||
|
||||
// 获取任务列表
|
||||
const {
|
||||
data: storageResponse,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["storage", pagination],
|
||||
queryFn: () =>
|
||||
GetAudioTask({ ...pagination })
|
||||
.then((res) => res.data)
|
||||
.catch((err) => {
|
||||
ErrorHandle(err);
|
||||
throw err;
|
||||
}),
|
||||
refetchInterval: 4000,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
// 删除任务
|
||||
const [delLoadings, setDelLoadings] = useState<number[]>([]);
|
||||
const { mutate: deleteMutation } = useMutation({
|
||||
mutationFn: DeleteAudioTask,
|
||||
onMutate: (id: number) => {
|
||||
setDelLoadings((prev) => [...prev, id]);
|
||||
},
|
||||
onSuccess: (_, ctx) => {
|
||||
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
|
||||
message.success("删除成功");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: Error, ctx) => {
|
||||
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
|
||||
ErrorHandle(error);
|
||||
},
|
||||
});
|
||||
// 批量删除任务
|
||||
const { mutate: deleteMutationAll, isPending: delAllLoadings } = useMutation({
|
||||
mutationFn: DeleteAudioTaskAll,
|
||||
onSuccess: () => {
|
||||
message.success("批量删除成功");
|
||||
refetch()
|
||||
setSelectedRowKeys([])
|
||||
},
|
||||
onError: ErrorHandle,
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 取消任务
|
||||
const [cancelLoadings, setCancelLoadings] = useState<number[]>([]);
|
||||
const { mutate: cancelMutation } = useMutation({
|
||||
mutationFn: GetAudioTaskCancel,
|
||||
onMutate: (id: number) => {
|
||||
// setDelLoadings((prev) => [...prev, id]);
|
||||
},
|
||||
onSuccess: (_, ctx) => {
|
||||
message.success("取消成功");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: Error, ctx) => {
|
||||
ErrorHandle(error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 处理分页变化
|
||||
const handleTableChange = (page: number, pageSize?: number) => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
page: page,
|
||||
size: pageSize || prev.size,
|
||||
}));
|
||||
};
|
||||
|
||||
// 客户端分页数据
|
||||
const dataSource = useMemo(() => {
|
||||
const items = storageResponse?.items || [];
|
||||
const start = (pagination.page - 1) * pagination.size;
|
||||
const end = start + pagination.size;
|
||||
return items.slice(start, end);
|
||||
}, [storageResponse, pagination]);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (
|
||||
newSelectedRowKeys: React.Key[],
|
||||
selectedRows: AudioTaskItem[]
|
||||
) => {
|
||||
setSelectedRowKeys([...newSelectedRowKeys]);
|
||||
},
|
||||
};
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<AudioTaskItem> = [
|
||||
{
|
||||
title: "任务编码",
|
||||
dataIndex: "id",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "音频名称",
|
||||
dataIndex: "audio_name",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "通道名称",
|
||||
dataIndex: "channel_name",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "通道编码",
|
||||
dataIndex: "channel_id",
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "mode",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{text}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{FormatFileSizeToString(text)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "时长",
|
||||
dataIndex: "duration",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{formatSecondsToHMS(text)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "task_status",
|
||||
align: "center",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{text == 0 && <Tag className="mr-0" color="gold">等待中</Tag>}
|
||||
{text == 1 && <Tag className="mr-0" color="green">执行中</Tag>}
|
||||
{text == 2 && <Tag className="mr-0" color="#87d068">已完成</Tag>}
|
||||
{text == 3 && <Tag className="mr-0" color="#999">已取消</Tag>}
|
||||
{text == 4 && <>
|
||||
<Tooltip placement="top" title={record.error_msg} color="#fff">
|
||||
<Tag className="mr-0" color="red">任务失败</Tag>
|
||||
</Tooltip>
|
||||
|
||||
</>}
|
||||
{text < 2 && <>
|
||||
<Tooltip placement="top" title="取消任务" color="#fff">
|
||||
<Button
|
||||
shape="circle"
|
||||
size="small"
|
||||
onClick={() => cancelMutation(record.id)}
|
||||
loading={cancelLoadings.includes(record.id)}
|
||||
icon={<CloseOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建日期",
|
||||
dataIndex: "created_at",
|
||||
align: "center",
|
||||
render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
|
||||
},
|
||||
// {
|
||||
// title: "开始日期",
|
||||
// dataIndex: "start_time",
|
||||
// align: "center",
|
||||
// render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
|
||||
// },
|
||||
// {
|
||||
// title: "结束日期",
|
||||
// dataIndex: "end_time",
|
||||
// align: "center",
|
||||
// render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
|
||||
// },
|
||||
|
||||
// {
|
||||
// title: "日期",
|
||||
// align: "center",
|
||||
// render: (_, record) => (
|
||||
// <>
|
||||
// <div><span>创建: </span>{record.created_at ? new Date(record.created_at).toLocaleString() : "-"}</div>
|
||||
// <div><span>更新: </span>{record.updated_at ? new Date(record.updated_at).toLocaleString() : "-"}</div>
|
||||
// </>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: "操作",
|
||||
align: "center",
|
||||
width: 120,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Popconfirm
|
||||
title="确定要删除这个文件吗?"
|
||||
onConfirm={() => {
|
||||
if (record.id) {
|
||||
deleteMutation(record.id);
|
||||
}
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
loading={delLoadings.includes(record.id)}
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center" className="mb-4">
|
||||
<Space>
|
||||
<Popconfirm
|
||||
title="确定要批量删除文件吗?"
|
||||
|
||||
onConfirm={() => {
|
||||
deleteMutationAll({ids:selectedRowKeys as number[]})
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button color="danger" variant="solid" loading={delAllLoadings} disabled={selectedRowKeys.length == 0} >
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
|
||||
<Filter
|
||||
searchLoading={isLoading}
|
||||
onSearchChange={(value: string) => {
|
||||
setPagination({ ...pagination, name: value });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
{/* 表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
rowSelection={rowSelection}
|
||||
dataSource={storageResponse?.items}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
scroll={{ x: "max-content" }}
|
||||
pagination={{
|
||||
current: pagination.page,
|
||||
pageSize: pagination.size,
|
||||
total: storageResponse?.total || 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: handleTableChange,
|
||||
onShowSizeChange: handleTableChange,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -67,7 +67,7 @@ const Filter: React.FC<IFilterProps> = ({
|
||||
<AutoSearch
|
||||
onSearch={onSearchChange}
|
||||
loading={searchLoading}
|
||||
placeholder={"请输入文件名称"}
|
||||
placeholder={"请输入关键字"}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
@ -5,7 +5,7 @@ import { CreateAudioEncode, UpdateAudioEncode } from "../api/audio";
|
||||
import type { CreateAudioEncodeReq, AudioEncodeItem } from "../types/audioencode";
|
||||
import { useGlobal } from "../Context";
|
||||
import ReactAudioPlayer from 'react-audio-player';
|
||||
|
||||
const apiAudioUrl = import.meta.env.VITE_AUDIO_BASE_URL;
|
||||
interface PlayerModalProps {
|
||||
title: string;
|
||||
onSuccess: () => void;
|
||||
@ -24,7 +24,7 @@ const PlayerModal = forwardRef<PlayerModalRef, PlayerModalProps>(
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (task?: AudioEncodeItem) => {
|
||||
if (task?.source_url) {
|
||||
setAudioUrl(task?.source_url)
|
||||
setAudioUrl(`${location.origin}${apiAudioUrl}${task?.source_url}`)
|
||||
}
|
||||
if (task?.name) {
|
||||
setAudioName(task?.name)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, Button, message, UploadFile, UploadProps } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||
// 音频文件上传组件
|
||||
const AudioUploader: React.FC<{
|
||||
onOk: ()=> void;
|
||||
@ -24,7 +24,7 @@ import { UploadOutlined } from '@ant-design/icons';
|
||||
formData.append('audio', file); // 与后端接收的参数名保持一致
|
||||
|
||||
// 发送请求到 Go 后端
|
||||
const response = await fetch('/api/audioencode/upload', {
|
||||
const response = await fetch(apiBaseUrl+'/audioencode/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Alert, ConfigProvider, Modal, Tag, message } from "antd";
|
||||
import { Space, ConfigProvider, Modal, Tag, Tooltip, Button } from "antd";
|
||||
import { DeliveredProcedureOutlined } from "@ant-design/icons";
|
||||
import Table, { ColumnsType } from "antd/es/table";
|
||||
import React, {
|
||||
forwardRef,
|
||||
@ -12,28 +13,30 @@ import { ChannelItem, ChannelReq } from "../../types/device";
|
||||
import Filter from "./Filter";
|
||||
import { GetChannels } from "../../api/devices";
|
||||
import { useGlobal } from "../../Context";
|
||||
|
||||
import type { AddTaskItem } from "../../types/audiotask";
|
||||
export interface IChannelModelFunc {
|
||||
openModal: (id: number) => void;
|
||||
openModal: (id: number, name: string) => void;
|
||||
}
|
||||
|
||||
interface IChannelModel {
|
||||
ref: any;
|
||||
onCallback: (id: any, bid:any) => void;
|
||||
onCallback: (data: AddTaskItem[]) => void;
|
||||
}
|
||||
|
||||
const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
const ChannelModel: React.FC<IChannelModel> = forwardRef(({ onCallback }, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
openModal: (id: string) => {
|
||||
openModal: (id: number, name: string) => {
|
||||
setOpen(true);
|
||||
if (id != "") {
|
||||
setSelectedRowKeys([id])
|
||||
}
|
||||
// if (id != 0) {
|
||||
// setSelectedRowKeys([id])
|
||||
// }
|
||||
audioName.current = name
|
||||
pid.current = id;
|
||||
},
|
||||
}));
|
||||
const [open, setOpen] = useState(false);
|
||||
const pid = useRef<string>(undefined);
|
||||
const pid = useRef<number>(0);
|
||||
const audioName = useRef<string>('');
|
||||
const { ErrorHandle } = useGlobal();
|
||||
|
||||
const columns: ColumnsType<ChannelItem> = [
|
||||
@ -74,6 +77,25 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
dataIndex: "protocol",
|
||||
render: (text: string) => text || "-",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
align: "center",
|
||||
width: 120,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Tooltip placement="top" title="下发广播任务" color="#fff">
|
||||
<Button icon={<DeliveredProcedureOutlined />} onClick={() => {
|
||||
onCallback([{
|
||||
audio_id: pid.current,
|
||||
channel_id: record.id,
|
||||
channel_name: record.name,
|
||||
}])
|
||||
}} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 获取通道列表
|
||||
@ -82,10 +104,10 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
size: 10, // 通道一般 < 10 个,客户端不做分页,一次性全查
|
||||
device_id: "",
|
||||
pid: "ROOT",
|
||||
status: "",
|
||||
status: true,
|
||||
name: "",
|
||||
bid: "",
|
||||
protocol:"GB28181"
|
||||
protocol: "GB28181"
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
@ -100,6 +122,7 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
enabled: open,
|
||||
});
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRowsAll, setSelectedRows] = useState<ChannelItem[]>([]);
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
getCheckboxProps: (record: ChannelItem) => ({
|
||||
@ -109,17 +132,34 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
newSelectedRowKeys: React.Key[],
|
||||
selectedRows: ChannelItem[]
|
||||
) => {
|
||||
if (newSelectedRowKeys.length > 0) {
|
||||
setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]);
|
||||
}
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
setSelectedRows(selectedRows)
|
||||
// if (newSelectedRowKeys.length > 0) {
|
||||
// setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]);
|
||||
// }
|
||||
},
|
||||
};
|
||||
|
||||
const onAll = () => {
|
||||
let dataItem: AddTaskItem[] = []
|
||||
selectedRowsAll.forEach(record => {
|
||||
let list: AddTaskItem = {
|
||||
audio_id: pid.current,
|
||||
channel_id: record.id,
|
||||
channel_name: record.name,
|
||||
}
|
||||
dataItem.push(list)
|
||||
});
|
||||
onCallback(dataItem)
|
||||
setOpen(false);
|
||||
setSelectedRows([])
|
||||
setSelectedRowKeys([])
|
||||
}
|
||||
const onCancel = () => {
|
||||
setOpen(false);
|
||||
if (selectedRowKeys.length>0) {
|
||||
onCallback(selectedRowKeys[0], pid.current)
|
||||
}
|
||||
// if (selectedRowKeys.length>0) {
|
||||
// onCallback(selectedRowKeys[0], pid.current)
|
||||
// }
|
||||
setSelectedRows([])
|
||||
setSelectedRowKeys([])
|
||||
};
|
||||
|
||||
@ -144,11 +184,17 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
>
|
||||
<Modal
|
||||
open={open}
|
||||
title="选中通道"
|
||||
title={`选择通道广播【${audioName.current}】音频`}
|
||||
style={{ top: "5%" }}
|
||||
width={"1000px"}
|
||||
onCancel={onCancel}
|
||||
onOk={onCancel}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => onCancel()} >关 闭</Button>
|
||||
<Button disabled={selectedRowKeys.length == 0} type="primary" onClick={() => onAll()} className="mr-6">批量下发</Button>
|
||||
</>
|
||||
}
|
||||
onCancel={onCancel}
|
||||
// onOk={onCancel}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 flex justify-end">
|
||||
@ -158,7 +204,7 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
|
||||
onSearchChange={(value: string) => {
|
||||
setPagination({ ...pagination, name: value, bid: value });
|
||||
}}
|
||||
onSelectChange={(value: string) => {
|
||||
onSelectChange={(value: any) => {
|
||||
setPagination({ ...pagination, status: value });
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
import Box from "../components/Box";
|
||||
import { useState } from "react";
|
||||
import AudioEncodePage from "../components/AudioEncode";
|
||||
import AudioTaskPage from "../components/AudioTask";
|
||||
|
||||
type MenuItem = Required<MenuProps>["items"][number];
|
||||
export default function Home() {
|
||||
@ -14,12 +15,12 @@ export default function Home() {
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
key: "sub0",
|
||||
label: "推流任务",
|
||||
label: "文件管理",
|
||||
icon: <FileSearchOutlined />,
|
||||
},
|
||||
{
|
||||
key: "sub1",
|
||||
label: "基础配置",
|
||||
label: "任务列表",
|
||||
icon: <UnorderedListOutlined />,
|
||||
},
|
||||
];
|
||||
@ -30,7 +31,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
{/* <div>
|
||||
<div>
|
||||
<Affix className="hidden lg:block" offsetTop={0}>
|
||||
<Box
|
||||
style={{ minWidth: "10rem", width: "12rem" }}
|
||||
@ -57,9 +58,9 @@ export default function Home() {
|
||||
items={items}
|
||||
/>
|
||||
</Box>
|
||||
</div> */}
|
||||
{/* <Col sm={24} md={24} lg={18} xl={18} xxl={18} className="w-full"> */}
|
||||
<Col sm={24} md={24} className="w-full">
|
||||
</div>
|
||||
<Col sm={24} md={24} lg={18} xl={18} xxl={18} className="w-full">
|
||||
{/* <Col sm={24} md={24} className="w-full"> */}
|
||||
{currentMenu == "sub0" && (
|
||||
<Box>
|
||||
<AudioEncodePage />
|
||||
@ -67,7 +68,7 @@ export default function Home() {
|
||||
)}
|
||||
{currentMenu == "sub1" && (
|
||||
<Box>
|
||||
基础配置
|
||||
<AudioTaskPage />
|
||||
</Box>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
83
web/src/types/audiotask.ts
Normal file
83
web/src/types/audiotask.ts
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
/**
|
||||
* 基础
|
||||
*/
|
||||
export type AudioTaskBaseRes = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询列表响应
|
||||
*/
|
||||
export type AudioTaskRes = {
|
||||
items: AudioTaskItem[];
|
||||
/**
|
||||
* 总数
|
||||
*/
|
||||
total: number;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 项
|
||||
*/
|
||||
export type AudioTaskItem = {
|
||||
id: number;
|
||||
audio_id: number;
|
||||
audio_name: string;
|
||||
channel_id: string;
|
||||
channel_name: string;
|
||||
size: number;
|
||||
duration: number;
|
||||
mode: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
task_status: number;
|
||||
end_time: string;
|
||||
create_time: string;
|
||||
start_time: string;
|
||||
error_msg: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建请求
|
||||
*/
|
||||
export type TaskReq = {
|
||||
/**
|
||||
* 名称模糊搜索
|
||||
*/
|
||||
audio_name?: string;
|
||||
/**
|
||||
* 页码(1~N)
|
||||
*/
|
||||
page: number;
|
||||
/**
|
||||
* 单页元素数量(10~100)
|
||||
*/
|
||||
size: number;
|
||||
|
||||
}
|
||||
export type AddTaskItem = {
|
||||
/**
|
||||
* 音频名称
|
||||
*/
|
||||
audio_id: number;
|
||||
/**
|
||||
* 通道名称
|
||||
*/
|
||||
channel_name: string;
|
||||
/**
|
||||
* 通道ID
|
||||
*/
|
||||
channel_id: string;
|
||||
};
|
||||
export type CreateAudioTaskReq = {
|
||||
items: AddTaskItem[];
|
||||
};
|
||||
|
||||
export type DelAudioTaskReq = {
|
||||
/**
|
||||
* IDs
|
||||
*/
|
||||
ids: number[];
|
||||
};
|
||||
2
web/src/types/device.d.ts
vendored
2
web/src/types/device.d.ts
vendored
@ -189,7 +189,7 @@ type ChannelReq = {
|
||||
/**
|
||||
* true:在线; false:离线;
|
||||
*/
|
||||
status?: string;
|
||||
status?: boolean;
|
||||
};
|
||||
|
||||
export type ChannelRes = {
|
||||
|
||||
19
web/src/utils/time.ts
Normal file
19
web/src/utils/time.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 将秒数格式化为 "HH:mm:ss" 格式
|
||||
* @param {number} totalSeconds - 总秒数
|
||||
* @returns {string} - 格式化后的时间字符串
|
||||
*/
|
||||
export function formatSecondsToHMS(totalSeconds: number) :string{
|
||||
if (totalSeconds <= 0) return "";
|
||||
// 计算小时、分钟和秒
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
|
||||
const seconds = totalSeconds - (hours * 3600) - (minutes * 60);
|
||||
|
||||
// 使用 padStart(2, '0') 来确保每个部分都是两位数,不足的用 '0' 填充
|
||||
const formattedHours = hours.toString().padStart(2, '0');
|
||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||
const formattedSeconds = seconds.toString().padStart(2, '0');
|
||||
|
||||
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user