Buffered video player
This commit is contained in:
parent
95e7203450
commit
f08e523f86
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"git.martyn.berlin/martyn/fyne-widgets/pkg/bufferedvid"
|
||||
"git.martyn.berlin/martyn/fyne-widgets/pkg/layouts"
|
||||
)
|
||||
|
||||
var videoWidget *bufferedvid.BufferedVidPlayback
|
||||
|
||||
func unimplemented() {}
|
||||
|
||||
func main() {
|
||||
a := app.New()
|
||||
w := a.NewWindow("Video")
|
||||
|
||||
videoWidget = bufferedvid.NewBufferedVidPlayback()
|
||||
videoWidget.VideoFilename = "1 Minute Timer-CH50zuS8DD0.mp4"
|
||||
layout := layouts.NewFloatingControlsLayout()
|
||||
layout.FloatingControlsLocation = layouts.FloatingControlsCenter
|
||||
|
||||
buttons := container.NewHBox(widget.NewButton(">", func() { videoWidget.Play() }), widget.NewButton("||", func() {
|
||||
if videoWidget.IsPaused() {
|
||||
videoWidget.UnPause()
|
||||
} else {
|
||||
videoWidget.Pause()
|
||||
}
|
||||
}))
|
||||
|
||||
w.SetContent(container.New(&layout, videoWidget, buttons))
|
||||
w.Resize(fyne.NewSize(640, 480))
|
||||
w.ShowAndRun()
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
package bufferedvid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/zergon321/reisen"
|
||||
)
|
||||
|
||||
func staticNoiseImage(w, h int) *image.RGBA {
|
||||
i := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for x := 0; x < w; x++ {
|
||||
for y := 0; y < h; y++ {
|
||||
lum := uint8(rand.Float32() * 255)
|
||||
i.Set(x, y, color.RGBA{lum, lum, lum, 255})
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
const (
|
||||
width = 1280
|
||||
height = 720
|
||||
frameBufferSize = 1024
|
||||
sampleRate = 44100
|
||||
channelCount = 2
|
||||
bitDepth = 8
|
||||
sampleBufferSize = 32 * channelCount * bitDepth * 1024
|
||||
)
|
||||
|
||||
// player holds all the data
|
||||
// necessary for playing video.
|
||||
type player struct {
|
||||
pix *image.NRGBA
|
||||
ticker <-chan time.Time
|
||||
errs <-chan error
|
||||
frameBuffer <-chan *image.RGBA
|
||||
audioBuffer <-chan [2]float64
|
||||
last time.Time
|
||||
fps int
|
||||
paused bool
|
||||
frameCount int64
|
||||
}
|
||||
|
||||
// readVideoAndAudio reads video and audio frames
|
||||
// from the opened media and sends the decoded
|
||||
// data to che channels to be played.
|
||||
func (p *player) readVideoAndAudio(media *reisen.Media) (<-chan *image.RGBA, <-chan [2]float64, chan error, error) {
|
||||
frameBuffer := make(chan *image.RGBA, frameBufferSize)
|
||||
sampleBuffer := make(chan [2]float64, sampleBufferSize)
|
||||
errs := make(chan error)
|
||||
|
||||
err := media.OpenDecode()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
videoStream := media.VideoStreams()[0]
|
||||
err = videoStream.Open()
|
||||
p.frameCount = videoStream.FrameCount()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
audioStream := media.AudioStreams()[0]
|
||||
err = audioStream.Open()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
packet, gotPacket, err := media.ReadPacket()
|
||||
|
||||
if err != nil {
|
||||
go func(err error) {
|
||||
errs <- err
|
||||
}(err)
|
||||
}
|
||||
|
||||
if !gotPacket {
|
||||
break
|
||||
}
|
||||
|
||||
switch packet.Type() {
|
||||
case reisen.StreamVideo:
|
||||
s := media.Streams()[packet.StreamIndex()].(*reisen.VideoStream)
|
||||
videoFrame, gotFrame, err := s.ReadVideoFrame()
|
||||
|
||||
if err != nil {
|
||||
go func(err error) {
|
||||
errs <- err
|
||||
}(err)
|
||||
}
|
||||
|
||||
if !gotFrame {
|
||||
break
|
||||
}
|
||||
|
||||
if videoFrame == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
frameBuffer <- videoFrame.Image()
|
||||
|
||||
case reisen.StreamAudio:
|
||||
s := media.Streams()[packet.StreamIndex()].(*reisen.AudioStream)
|
||||
audioFrame, gotFrame, err := s.ReadAudioFrame()
|
||||
|
||||
if err != nil {
|
||||
go func(err error) {
|
||||
errs <- err
|
||||
}(err)
|
||||
}
|
||||
|
||||
if !gotFrame {
|
||||
break
|
||||
}
|
||||
|
||||
if audioFrame == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Turn the raw byte data into
|
||||
// audio samples of type [2]float64.
|
||||
reader := bytes.NewReader(audioFrame.Data())
|
||||
|
||||
// See the README.md file for
|
||||
// detailed scheme of the sample structure.
|
||||
for reader.Len() > 0 {
|
||||
sample := [2]float64{0, 0}
|
||||
var result float64
|
||||
err = binary.Read(reader, binary.LittleEndian, &result)
|
||||
|
||||
if err != nil {
|
||||
go func(err error) {
|
||||
errs <- err
|
||||
}(err)
|
||||
}
|
||||
|
||||
sample[0] = result
|
||||
|
||||
err = binary.Read(reader, binary.LittleEndian, &result)
|
||||
|
||||
if err != nil {
|
||||
go func(err error) {
|
||||
errs <- err
|
||||
}(err)
|
||||
}
|
||||
|
||||
sample[1] = result
|
||||
sampleBuffer <- sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoStream.Close()
|
||||
audioStream.Close()
|
||||
media.CloseDecode()
|
||||
close(frameBuffer)
|
||||
close(sampleBuffer)
|
||||
close(errs)
|
||||
}()
|
||||
|
||||
return frameBuffer, sampleBuffer, errs, nil
|
||||
}
|
||||
|
||||
// Starts reading samples and frames
|
||||
// of the media file.
|
||||
func (p *player) open(fname string) error {
|
||||
// Sprite for drawing video frames.
|
||||
p.pix = image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Open the media file.
|
||||
media, err := reisen.NewMedia(fname)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the FPS for playing
|
||||
// video frames.
|
||||
videoFPS, _ := media.Streams()[0].FrameRate()
|
||||
fmt.Printf("Detected FPS of %d\n", videoFPS)
|
||||
if videoFPS == 0 {
|
||||
fmt.Println("Setting fps to 60 as no FPS found")
|
||||
videoFPS = 60
|
||||
}
|
||||
if videoFPS > 150 {
|
||||
fmt.Printf("FPS of %d seems way too high, assuming it's *1000, so we'll use %d\n", videoFPS, videoFPS/1000)
|
||||
videoFPS = videoFPS / 1000
|
||||
}
|
||||
p.fps = videoFPS
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// SPF for frame ticker.
|
||||
spf := 1.0 / float64(videoFPS)
|
||||
frameDuration, err := time.
|
||||
ParseDuration(fmt.Sprintf("%fs", spf))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Start decoding streams.
|
||||
p.frameBuffer, p.audioBuffer,
|
||||
p.errs, err = p.readVideoAndAudio(media)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.ticker = time.Tick(frameDuration)
|
||||
|
||||
// Setup metrics.
|
||||
p.last = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ fyne.WidgetRenderer = (*bufferedVidPlaybackRenderer)(nil)
|
||||
|
||||
type VideoControlsVisibleStruct struct {
|
||||
PlayButton bool
|
||||
PauseButton bool
|
||||
Scrubber bool
|
||||
}
|
||||
|
||||
type BufferedVidPlayback struct {
|
||||
widget.BaseWidget
|
||||
UpdateFPS int64
|
||||
VideoFilename string
|
||||
VideoControlsVisible VideoControlsVisibleStruct
|
||||
Loop bool
|
||||
|
||||
fpsTimer *time.Ticker
|
||||
videoOpened bool
|
||||
playerStruct player
|
||||
currentFrameID int64
|
||||
allFrames []image.RGBA
|
||||
bufferFilling bool
|
||||
paused bool
|
||||
}
|
||||
|
||||
func NewBufferedVidPlayback() *BufferedVidPlayback {
|
||||
w := &BufferedVidPlayback{}
|
||||
w.ExtendBaseWidget(w)
|
||||
w.UpdateFPS = 25
|
||||
w.videoOpened = false
|
||||
w.currentFrameID = 0
|
||||
w.bufferFilling = false
|
||||
w.paused = false
|
||||
w.Loop = false
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func fileIsOpenable(path string) bool {
|
||||
_, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) Play() {
|
||||
if !w.videoOpened {
|
||||
if fileIsOpenable(w.VideoFilename) {
|
||||
w.playerStruct.open(w.VideoFilename)
|
||||
w.fpsTimer.Stop()
|
||||
spf := 1.0 / float64(w.playerStruct.fps)
|
||||
frameDuration, err := time.
|
||||
ParseDuration(fmt.Sprintf("%fs", spf))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error %s\n", err.Error())
|
||||
}
|
||||
w.fpsTimer.Reset(frameDuration)
|
||||
go func() {
|
||||
// empty the audio buffer in case that's what's jamming things up.
|
||||
for {
|
||||
_ = <-w.playerStruct.audioBuffer
|
||||
}
|
||||
}()
|
||||
w.FillBuffer()
|
||||
w.videoOpened = true
|
||||
}
|
||||
}
|
||||
if w.paused {
|
||||
w.paused = false
|
||||
}
|
||||
w.BaseWidget.Refresh()
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) Pause() {
|
||||
if !w.videoOpened || !w.bufferFilling {
|
||||
if !w.paused {
|
||||
w.paused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) UnPause() {
|
||||
if !w.videoOpened || !w.bufferFilling {
|
||||
if w.paused {
|
||||
w.paused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) IsPaused() bool {
|
||||
return w.paused
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) Resize(s fyne.Size) {
|
||||
w.BaseWidget.Resize(s)
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) FillBuffer() {
|
||||
w.allFrames = make([]image.RGBA, w.playerStruct.frameCount)
|
||||
w.bufferFilling = true
|
||||
w.currentFrameID = 0
|
||||
for {
|
||||
frameImage := <-w.playerStruct.frameBuffer
|
||||
w.allFrames[w.currentFrameID] = *frameImage
|
||||
w.currentFrameID = w.currentFrameID + 1
|
||||
if w.currentFrameID > w.playerStruct.frameCount-2 {
|
||||
w.currentFrameID = 0
|
||||
w.bufferFilling = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufferedVidPlayback) CreateRenderer() fyne.WidgetRenderer {
|
||||
w.fpsTimer = time.NewTicker(time.Duration(1000/25) * time.Millisecond)
|
||||
go func(v *BufferedVidPlayback) {
|
||||
for {
|
||||
_ = <-w.fpsTimer.C
|
||||
v.Refresh()
|
||||
}
|
||||
}(w)
|
||||
return newBufferedVidPlaybackRenderer(w)
|
||||
}
|
||||
|
||||
type bufferedVidPlaybackRenderer struct {
|
||||
bufferedVidPlayback *BufferedVidPlayback
|
||||
currentframe *canvas.Raster
|
||||
progress *widget.Slider
|
||||
bufferingProgress *widget.ProgressBar
|
||||
pausebtn, playbtn *widget.Button
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) actualrenderframe(w, h int) image.Image {
|
||||
var frameImage *image.RGBA
|
||||
r.bufferingProgress.Hide()
|
||||
if r.bufferedVidPlayback.bufferFilling {
|
||||
r.bufferingProgress.Show()
|
||||
r.bufferingProgress.Min = 0
|
||||
r.bufferingProgress.Max = float64(r.bufferedVidPlayback.playerStruct.frameCount)
|
||||
r.bufferingProgress.Value = float64(r.bufferedVidPlayback.currentFrameID)
|
||||
frameImage = staticNoiseImage(w, h)
|
||||
} else if r.bufferedVidPlayback.videoOpened {
|
||||
frameImage = &r.bufferedVidPlayback.allFrames[r.bufferedVidPlayback.currentFrameID]
|
||||
if !r.bufferedVidPlayback.paused {
|
||||
r.bufferedVidPlayback.currentFrameID = r.bufferedVidPlayback.currentFrameID + 1
|
||||
if r.bufferedVidPlayback.currentFrameID > r.bufferedVidPlayback.playerStruct.frameCount-5 { //definitely stopping before the end
|
||||
r.bufferedVidPlayback.currentFrameID = 0
|
||||
if !r.bufferedVidPlayback.Loop {
|
||||
r.bufferedVidPlayback.paused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frameImage = staticNoiseImage(w, h) //no point doing anything other than returning anyway!
|
||||
}
|
||||
return frameImage
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) seek(frame float64) {
|
||||
if r.bufferedVidPlayback.videoOpened && !r.bufferedVidPlayback.bufferFilling {
|
||||
r.bufferedVidPlayback.currentFrameID = int64(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func newBufferedVidPlaybackRenderer(w *BufferedVidPlayback) *bufferedVidPlaybackRenderer {
|
||||
renderer := &bufferedVidPlaybackRenderer{
|
||||
bufferedVidPlayback: w,
|
||||
}
|
||||
renderer.currentframe = canvas.NewRaster(renderer.actualrenderframe)
|
||||
renderer.progress = widget.NewSlider(0, 1)
|
||||
renderer.progress.OnChanged = renderer.seek
|
||||
renderer.playbtn = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() { w.Play() })
|
||||
renderer.pausebtn = widget.NewButtonWithIcon("", theme.MediaPauseIcon(), func() {
|
||||
if w.paused {
|
||||
w.UnPause()
|
||||
} else {
|
||||
w.Pause()
|
||||
}
|
||||
})
|
||||
renderer.bufferingProgress = widget.NewProgressBar()
|
||||
renderer.bufferingProgress.TextFormatter = func() string {
|
||||
if w.playerStruct.frameCount > 0 {
|
||||
return fmt.Sprintf("Buffering (%2.2f%%)...", float64(w.currentFrameID)/float64(w.playerStruct.frameCount)*100)
|
||||
}
|
||||
return "Buffering..."
|
||||
}
|
||||
return renderer
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) Objects() []fyne.CanvasObject {
|
||||
// The order is critical, rect is drawn first then currentframe
|
||||
return []fyne.CanvasObject{r.currentframe, r.bufferingProgress, r.playbtn, r.pausebtn, r.progress}
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) Layout(s fyne.Size) {
|
||||
videoControlsMaxMin := fyne.Max(r.progress.MinSize().Height, r.pausebtn.MinSize().Height)
|
||||
videoControlsY := s.Height - videoControlsMaxMin
|
||||
r.currentframe.Resize(fyne.NewSize(s.Width, videoControlsY))
|
||||
r.currentframe.Move(fyne.NewPos(0, 0))
|
||||
r.bufferingProgress.Resize(fyne.NewSize(s.Width, r.bufferingProgress.MinSize().Height))
|
||||
r.bufferingProgress.Move(fyne.NewPos(0, (s.Height/2)-(r.bufferingProgress.Size().Height)))
|
||||
r.playbtn.Resize(r.playbtn.MinSize())
|
||||
r.playbtn.Move(fyne.NewPos(0, videoControlsY))
|
||||
if fyne.Max(r.progress.MinSize().Height, r.playbtn.MinSize().Height) == r.progress.MinSize().Height {
|
||||
r.playbtn.Resize(fyne.NewSize(r.playbtn.Size().Width, r.progress.Size().Height))
|
||||
}
|
||||
r.pausebtn.Resize(r.pausebtn.MinSize())
|
||||
r.pausebtn.Move(fyne.NewPos(r.pausebtn.MinSize().Width, videoControlsY))
|
||||
if fyne.Max(r.progress.MinSize().Height, r.pausebtn.MinSize().Height) == r.progress.MinSize().Height {
|
||||
r.pausebtn.Resize(fyne.NewSize(r.pausebtn.Size().Width, r.progress.Size().Height))
|
||||
}
|
||||
r.progress.Resize(fyne.NewSize(s.Width-r.playbtn.MinSize().Width-r.pausebtn.MinSize().Width, r.progress.MinSize().Height))
|
||||
r.progress.Move(fyne.NewPos(r.playbtn.MinSize().Width+r.pausebtn.MinSize().Width, videoControlsY))
|
||||
if fyne.Max(r.progress.MinSize().Height, r.pausebtn.MinSize().Height) == r.pausebtn.MinSize().Height {
|
||||
r.progress.Resize(fyne.NewSize(r.progress.Size().Width, r.pausebtn.Size().Height))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) MinSize() fyne.Size {
|
||||
return fyne.NewSize(200, 200)
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) Refresh() {
|
||||
r.currentframe.Refresh()
|
||||
r.progress.Min = 0
|
||||
r.progress.Max = float64(r.bufferedVidPlayback.playerStruct.frameCount)
|
||||
if r.bufferedVidPlayback.bufferFilling {
|
||||
r.progress.Value = 0
|
||||
} else {
|
||||
r.progress.Value = float64(r.bufferedVidPlayback.currentFrameID)
|
||||
}
|
||||
r.bufferingProgress.Refresh()
|
||||
r.currentframe.Refresh()
|
||||
r.progress.Refresh()
|
||||
r.pausebtn.Refresh()
|
||||
r.playbtn.Refresh()
|
||||
}
|
||||
|
||||
func (r *bufferedVidPlaybackRenderer) Destroy() {
|
||||
r.bufferedVidPlayback.fpsTimer.Stop()
|
||||
} // Called when the renderer is destroyed
|
Loading…
Reference in New Issue