Implement in-app playback with ffmpeg frame pump and Go audio
This commit is contained in:
parent
e054a39c97
commit
b26c4183fd
7
go.mod
7
go.mod
|
|
@ -4,13 +4,12 @@ go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.1
|
fyne.io/fyne/v2 v2.7.1
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0
|
github.com/hajimehoshi/oto v0.7.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
|
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
|
||||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
github.com/aws/aws-sdk-go v1.38.20 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fredbi/uri v1.1.1 // indirect
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
|
@ -26,7 +25,6 @@ require (
|
||||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
|
|
@ -36,9 +34,10 @@ require (
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/u2takey/go-utils v0.3.1 // indirect
|
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||||
golang.org/x/image v0.24.0 // indirect
|
golang.org/x/image v0.24.0 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
|
|
||||||
65
go.sum
65
go.sum
|
|
@ -39,6 +39,8 @@ github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQb
|
||||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||||
|
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||||
|
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||||
|
|
@ -65,12 +67,21 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
|
||||||
|
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
@ -78,57 +89,3 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA=
|
|
||||||
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
|
||||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPizyZiYoo=
|
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
|
||||||
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
|
||||||
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
|
||||||
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
|
||||||
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
|
||||||
|
|
|
||||||
361
main.go
361
main.go
|
|
@ -6,8 +6,10 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -16,6 +18,7 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
|
|
@ -28,6 +31,7 @@ import (
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
|
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
|
||||||
|
"github.com/hajimehoshi/oto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module describes a high level tool surface that gets a tile on the menu.
|
// Module describes a high level tool surface that gets a tile on the menu.
|
||||||
|
|
@ -129,6 +133,7 @@ type appState struct {
|
||||||
playerMuted bool
|
playerMuted bool
|
||||||
lastVolume float64
|
lastVolume float64
|
||||||
playerPaused bool
|
playerPaused bool
|
||||||
|
playSess *playSession
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) stopPreview() {
|
func (s *appState) stopPreview() {
|
||||||
|
|
@ -212,6 +217,10 @@ func (s *appState) shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) stopPlayer() {
|
func (s *appState) stopPlayer() {
|
||||||
|
if s.playSess != nil {
|
||||||
|
s.playSess.Stop()
|
||||||
|
s.playSess = nil
|
||||||
|
}
|
||||||
if s.player != nil {
|
if s.player != nil {
|
||||||
s.player.Stop()
|
s.player.Stop()
|
||||||
}
|
}
|
||||||
|
|
@ -870,18 +879,43 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
dlg.Show()
|
dlg.Show()
|
||||||
})
|
})
|
||||||
|
|
||||||
usePlayer := state.playerReady && state.player != nil
|
usePlayer := true
|
||||||
|
|
||||||
currentTime := widget.NewLabel("0:00")
|
currentTime := widget.NewLabel("0:00")
|
||||||
totalTime := widget.NewLabel(src.DurationString())
|
totalTime := widget.NewLabel(src.DurationString())
|
||||||
totalTime.Alignment = fyne.TextAlignTrailing
|
totalTime.Alignment = fyne.TextAlignTrailing
|
||||||
|
var updatingProgress bool
|
||||||
|
slider := widget.NewSlider(0, math.Max(1, src.Duration))
|
||||||
|
slider.Step = 0.5
|
||||||
|
updateProgress := func(val float64) {
|
||||||
|
updatingProgress = true
|
||||||
|
currentTime.SetText(formatClock(val))
|
||||||
|
slider.SetValue(val)
|
||||||
|
updatingProgress = false
|
||||||
|
}
|
||||||
|
if state.playSess != nil {
|
||||||
|
state.playSess.prog = updateProgress
|
||||||
|
}
|
||||||
|
if state.playSess == nil {
|
||||||
|
state.playSess = newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, int(targetWidth-28), int(targetHeight-40), updateProgress, img)
|
||||||
|
}
|
||||||
|
|
||||||
var controls fyne.CanvasObject
|
var controls fyne.CanvasObject
|
||||||
if usePlayer {
|
if usePlayer {
|
||||||
var updatingVolume bool
|
|
||||||
var slider *widget.Slider
|
|
||||||
var volIcon *widget.Button
|
var volIcon *widget.Button
|
||||||
|
var updatingVolume bool
|
||||||
|
seek := func(val float64) {
|
||||||
|
if state.playSess != nil {
|
||||||
|
state.playSess.Seek(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slider.OnChanged = func(val float64) {
|
||||||
|
if updatingProgress {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateProgress(val)
|
||||||
|
seek(val)
|
||||||
|
}
|
||||||
updateVolIcon := func() {
|
updateVolIcon := func() {
|
||||||
if volIcon == nil {
|
if volIcon == nil {
|
||||||
return
|
return
|
||||||
|
|
@ -892,19 +926,8 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
volIcon.SetText("🔊")
|
volIcon.SetText("🔊")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slider = widget.NewSlider(0, math.Max(1, src.Duration))
|
|
||||||
slider.Step = 0.5
|
|
||||||
slider.OnChanged = func(val float64) {
|
|
||||||
currentTime.SetText(formatClock(val))
|
|
||||||
if state.player != nil && state.playerReady {
|
|
||||||
if err := state.player.Seek(val); err != nil {
|
|
||||||
debugLog(logCatFFMPEG, "player seek failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
volIcon = makeIconButton("🔊", "Mute/Unmute", func() {
|
volIcon = makeIconButton("🔊", "Mute/Unmute", func() {
|
||||||
if state.player == nil {
|
if state.playSess == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if state.playerMuted {
|
if state.playerMuted {
|
||||||
|
|
@ -914,18 +937,16 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
}
|
}
|
||||||
state.playerVolume = target
|
state.playerVolume = target
|
||||||
state.playerMuted = false
|
state.playerMuted = false
|
||||||
updatingVolume = true
|
if state.playSess != nil {
|
||||||
slider.SetValue(target)
|
state.playSess.SetVolume(target)
|
||||||
updatingVolume = false
|
}
|
||||||
_ = state.player.SetVolume(target)
|
|
||||||
} else {
|
} else {
|
||||||
state.lastVolume = state.playerVolume
|
state.lastVolume = state.playerVolume
|
||||||
state.playerVolume = 0
|
state.playerVolume = 0
|
||||||
state.playerMuted = true
|
state.playerMuted = true
|
||||||
updatingVolume = true
|
if state.playSess != nil {
|
||||||
slider.SetValue(0)
|
state.playSess.SetVolume(0)
|
||||||
updatingVolume = false
|
}
|
||||||
_ = state.player.SetVolume(0)
|
|
||||||
}
|
}
|
||||||
updateVolIcon()
|
updateVolIcon()
|
||||||
})
|
})
|
||||||
|
|
@ -943,39 +964,28 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
||||||
} else {
|
} else {
|
||||||
state.playerMuted = true
|
state.playerMuted = true
|
||||||
}
|
}
|
||||||
if state.player != nil && state.playerReady {
|
if state.playSess != nil {
|
||||||
if err := state.player.SetVolume(val); err != nil {
|
state.playSess.SetVolume(val)
|
||||||
debugLog(logCatFFMPEG, "player volume failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateVolIcon()
|
updateVolIcon()
|
||||||
}
|
}
|
||||||
updateVolIcon()
|
updateVolIcon()
|
||||||
volSlider.Refresh()
|
volSlider.Refresh()
|
||||||
playBtn := makeIconButton("▶/⏸", "Play/Pause", func() {
|
playBtn := makeIconButton("▶/⏸", "Play/Pause", func() {
|
||||||
if state.player == nil {
|
if state.playSess == nil {
|
||||||
return
|
state.playSess = newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, int(targetWidth-28), int(targetHeight-40), updateProgress, img)
|
||||||
|
state.playSess.SetVolume(state.playerVolume)
|
||||||
}
|
}
|
||||||
if state.playerPaused {
|
if state.playerPaused {
|
||||||
if err := state.player.Play(); err != nil {
|
state.playSess.Play()
|
||||||
debugLog(logCatFFMPEG, "player play failed: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.playerPaused = false
|
state.playerPaused = false
|
||||||
} else {
|
} else {
|
||||||
if err := state.player.Pause(); err != nil {
|
state.playSess.Pause()
|
||||||
debugLog(logCatFFMPEG, "player pause failed: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.playerPaused = true
|
state.playerPaused = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fullBtn := makeIconButton("⛶", "Toggle fullscreen", func() {
|
fullBtn := makeIconButton("⛶", "Toggle fullscreen", func() {
|
||||||
if state.player != nil {
|
// Placeholder: embed fullscreen toggle into playback surface later.
|
||||||
if err := state.player.FullScreen(); err != nil {
|
|
||||||
debugLog(logCatFFMPEG, "player fullscreen failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
volBox := container.NewHBox(volIcon, container.NewMax(volSlider))
|
volBox := container.NewHBox(volIcon, container.NewMax(volSlider))
|
||||||
progress := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider))
|
progress := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider))
|
||||||
|
|
@ -1049,6 +1059,260 @@ func moduleColor(id string) color.Color {
|
||||||
return queueColor
|
return queueColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type playSession struct {
|
||||||
|
path string
|
||||||
|
fps float64
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
targetW int
|
||||||
|
targetH int
|
||||||
|
volume float64
|
||||||
|
muted bool
|
||||||
|
paused bool
|
||||||
|
current float64
|
||||||
|
stop chan struct{}
|
||||||
|
done chan struct{}
|
||||||
|
prog func(float64)
|
||||||
|
img *canvas.Image
|
||||||
|
mu sync.Mutex
|
||||||
|
videoCmd *exec.Cmd
|
||||||
|
audioCmd *exec.Cmd
|
||||||
|
audioCtx *oto.Context
|
||||||
|
audioPlay io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, prog func(float64), img *canvas.Image) *playSession {
|
||||||
|
if fps <= 0 {
|
||||||
|
fps = 24
|
||||||
|
}
|
||||||
|
if targetW <= 0 {
|
||||||
|
targetW = 640
|
||||||
|
}
|
||||||
|
if targetH <= 0 {
|
||||||
|
targetH = int(float64(targetW) * (float64(h) / float64(maxInt(w, 1))))
|
||||||
|
}
|
||||||
|
return &playSession{
|
||||||
|
path: path,
|
||||||
|
fps: fps,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
targetW: targetW,
|
||||||
|
targetH: targetH,
|
||||||
|
volume: 100,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
prog: prog,
|
||||||
|
img: img,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) Play() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if p.videoCmd == nil && p.audioCmd == nil {
|
||||||
|
p.startLocked(p.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.paused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) Pause() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.paused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) Seek(offset float64) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.current = offset
|
||||||
|
p.stopLocked()
|
||||||
|
p.startLocked(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) SetVolume(v float64) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if v < 0 {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
if v > 100 {
|
||||||
|
v = 100
|
||||||
|
}
|
||||||
|
p.volume = v
|
||||||
|
if v > 0 {
|
||||||
|
p.muted = false
|
||||||
|
} else {
|
||||||
|
p.muted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) Stop() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.stopLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) stopLocked() {
|
||||||
|
select {
|
||||||
|
case <-p.stop:
|
||||||
|
default:
|
||||||
|
close(p.stop)
|
||||||
|
}
|
||||||
|
if p.videoCmd != nil && p.videoCmd.Process != nil {
|
||||||
|
_ = p.videoCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
if p.audioCmd != nil && p.audioCmd.Process != nil {
|
||||||
|
_ = p.audioCmd.Process.Kill()
|
||||||
|
}
|
||||||
|
if p.audioPlay != nil {
|
||||||
|
p.audioPlay.Close()
|
||||||
|
}
|
||||||
|
if p.audioCtx != nil {
|
||||||
|
p.audioCtx.Close()
|
||||||
|
}
|
||||||
|
p.videoCmd = nil
|
||||||
|
p.audioCmd = nil
|
||||||
|
p.audioPlay = nil
|
||||||
|
p.audioCtx = nil
|
||||||
|
p.stop = make(chan struct{})
|
||||||
|
p.done = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) startLocked(offset float64) {
|
||||||
|
p.paused = false
|
||||||
|
p.current = offset
|
||||||
|
p.runVideo(offset)
|
||||||
|
p.runAudio(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) runVideo(offset float64) {
|
||||||
|
cmd := exec.Command("ffmpeg",
|
||||||
|
"-hide_banner", "-loglevel", "error",
|
||||||
|
"-ss", fmt.Sprintf("%.3f", offset),
|
||||||
|
"-i", p.path,
|
||||||
|
"-vf", fmt.Sprintf("scale=%d:%d", p.targetW, p.targetH),
|
||||||
|
"-f", "rawvideo",
|
||||||
|
"-pix_fmt", "rgb24",
|
||||||
|
"-r", fmt.Sprintf("%.3f", p.fps),
|
||||||
|
"-",
|
||||||
|
)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.videoCmd = cmd
|
||||||
|
frameSize := p.targetW * p.targetH * 3
|
||||||
|
buf := make([]byte, frameSize)
|
||||||
|
go func() {
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if p.paused {
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := io.ReadFull(stdout, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frame := image.NewNRGBA(image.Rect(0, 0, p.targetW, p.targetH))
|
||||||
|
copy(frame.Pix, buf)
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
if p.img != nil {
|
||||||
|
p.img.Image = frame
|
||||||
|
p.img.Refresh()
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
p.current += 1.0 / p.fps
|
||||||
|
if p.prog != nil {
|
||||||
|
p.prog(p.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playSession) runAudio(offset float64) {
|
||||||
|
const sampleRate = 48000
|
||||||
|
const channels = 2
|
||||||
|
const bytesPerSample = 2
|
||||||
|
cmd := exec.Command("ffmpeg",
|
||||||
|
"-hide_banner", "-loglevel", "error",
|
||||||
|
"-ss", fmt.Sprintf("%.3f", offset),
|
||||||
|
"-i", p.path,
|
||||||
|
"-vn",
|
||||||
|
"-ac", fmt.Sprintf("%d", channels),
|
||||||
|
"-ar", fmt.Sprintf("%d", sampleRate),
|
||||||
|
"-f", "s16le",
|
||||||
|
"-",
|
||||||
|
)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.audioCmd = cmd
|
||||||
|
ctx, err := oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player := ctx.NewPlayer()
|
||||||
|
p.audioCtx = ctx
|
||||||
|
p.audioPlay = player
|
||||||
|
go func() {
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
defer player.Close()
|
||||||
|
defer ctx.Close()
|
||||||
|
chunk := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if p.paused {
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n, err := stdout.Read(chunk)
|
||||||
|
if n > 0 {
|
||||||
|
gain := p.volume / 100.0
|
||||||
|
if p.muted || gain <= 0 {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
chunk[i] = 0
|
||||||
|
}
|
||||||
|
} else if gain < 0.999 || gain > 1.001 {
|
||||||
|
for i := 0; i+1 < n; i += 2 {
|
||||||
|
sample := int16(chunk[i]) | int16(chunk[i+1])<<8
|
||||||
|
amp := int(float64(sample) * gain)
|
||||||
|
if amp > math.MaxInt16 {
|
||||||
|
amp = math.MaxInt16
|
||||||
|
}
|
||||||
|
if amp < math.MinInt16 {
|
||||||
|
amp = math.MinInt16
|
||||||
|
}
|
||||||
|
chunk[i] = byte(amp)
|
||||||
|
chunk[i+1] = byte(amp >> 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.Write(chunk[:n])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
type previewAnimator struct {
|
type previewAnimator struct {
|
||||||
frames []string
|
frames []string
|
||||||
img *canvas.Image
|
img *canvas.Image
|
||||||
|
|
@ -1184,6 +1448,10 @@ func (s *appState) handleDrop(items []fyne.URI) {
|
||||||
|
|
||||||
func (s *appState) loadVideo(path string) {
|
func (s *appState) loadVideo(path string) {
|
||||||
win := s.window
|
win := s.window
|
||||||
|
if s.playSess != nil {
|
||||||
|
s.playSess.Stop()
|
||||||
|
s.playSess = nil
|
||||||
|
}
|
||||||
src, err := probeVideo(path)
|
src, err := probeVideo(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugLog(logCatFFMPEG, "ffprobe failed for %s: %v", path, err)
|
debugLog(logCatFFMPEG, "ffprobe failed for %s: %v", path, err)
|
||||||
|
|
@ -1515,6 +1783,13 @@ func channelLabel(ch int) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func makeIconButton(symbol, tooltip string, tapped func()) *widget.Button {
|
func makeIconButton(symbol, tooltip string, tapped func()) *widget.Button {
|
||||||
btn := widget.NewButton(symbol, tapped)
|
btn := widget.NewButton(symbol, tapped)
|
||||||
btn.Importance = widget.LowImportance
|
btn.Importance = widget.LowImportance
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user