mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-26 03:38:55 +01:00
examples: add an example to play a video
Closes #110 Updates #1768 Updates ebitengine/oto#235
This commit is contained in:
parent
ac6c346c8b
commit
d3befbf89b
9
examples/video/license.md
Normal file
9
examples/video/license.md
Normal file
@ -0,0 +1,9 @@
|
||||
# License
|
||||
|
||||
## `shibuya.mpg`, `shibuya_noaudio.mpg`
|
||||
|
||||
https://commons.wikimedia.org/wiki/File:Shibuya_Crossing,_Tokyo,_Japan_(video).webm
|
||||
|
||||
"Shibuya Crossing, Tokyo, Japan (video).webm" by Basile Morin
|
||||
|
||||
The Creative Commons Attribution-Share Alike 4.0 International license
|
87
examples/video/main.go
Normal file
87
examples/video/main.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright 2024 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/audio"
|
||||
)
|
||||
|
||||
//go:embed shibuya.mpg
|
||||
var shibuya_mpg []byte
|
||||
|
||||
type Game struct {
|
||||
player *mpegPlayer
|
||||
}
|
||||
|
||||
func (g *Game) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
g.player.Draw(screen)
|
||||
}
|
||||
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
|
||||
return outsideWidth, outsideHeight
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Initialize audio context.
|
||||
_ = audio.NewContext(48000)
|
||||
|
||||
// If you want to play your own video, the video must be an MPEG-1 video with 48000 audio sample rate.
|
||||
// You can convert the video to MPEG-1 with the below command:
|
||||
//
|
||||
// ffmpeg -i YOUR_VIDEO -c:v mpeg1video -q:v 8 -c:a mp2 -format mpeg -ar 48000 output.mpg
|
||||
//
|
||||
// You can adjust quality by changing -q:v value. A lower value indicates better quality.
|
||||
var in io.ReadSeeker
|
||||
if len(os.Args) > 1 {
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
in = f
|
||||
} else {
|
||||
in = bytes.NewReader(shibuya_mpg)
|
||||
fmt.Println("Play the default video. You can specify a video file as an argument.")
|
||||
}
|
||||
|
||||
player, err := newMPEGPlayer(in)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
g := &Game{
|
||||
player: player,
|
||||
}
|
||||
player.Play()
|
||||
|
||||
ebiten.SetWindowTitle("Video (Ebiten Demo)")
|
||||
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
|
||||
if err := ebiten.RunGame(g); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
220
examples/video/mpegplayer.go
Normal file
220
examples/video/mpegplayer.go
Normal file
@ -0,0 +1,220 @@
|
||||
// Copyright 2024 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/mpeg"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/audio"
|
||||
)
|
||||
|
||||
type mpegPlayer struct {
|
||||
mpg *mpeg.MPEG
|
||||
|
||||
currentFrame *ebiten.Image
|
||||
audioPlayer *audio.Player
|
||||
|
||||
// These members are used when the video doesn't have an audio stream.
|
||||
refTime time.Time
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func newMPEGPlayer(src io.Reader) (*mpegPlayer, error) {
|
||||
mpg, err := mpeg.New(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mpg.NumVideoStreams() == 0 {
|
||||
return nil, fmt.Errorf("video: no video streams")
|
||||
}
|
||||
if !mpg.HasHeaders() {
|
||||
return nil, fmt.Errorf("video: missing headers")
|
||||
}
|
||||
|
||||
p := &mpegPlayer{
|
||||
mpg: mpg,
|
||||
currentFrame: ebiten.NewImage(mpg.Width(), mpg.Height()),
|
||||
}
|
||||
|
||||
// If the video doesn't have an audio stream, initialization is done.
|
||||
if mpg.NumAudioStreams() == 0 {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// If the video has an audio stream, initialize an audio player.
|
||||
ctx := audio.CurrentContext()
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("video: audio.Context is not initialized")
|
||||
}
|
||||
if mpg.Channels() != 2 {
|
||||
return nil, fmt.Errorf("video: mpeg audio stream must be 2 but was %d", mpg.Channels())
|
||||
}
|
||||
if ctx.SampleRate() != mpg.Samplerate() {
|
||||
return nil, fmt.Errorf("video: mpeg audio stream sample rate %d doesn't match with audio context sample rate %d", mpg.Samplerate(), ctx.SampleRate())
|
||||
}
|
||||
|
||||
mpg.SetAudioFormat(mpeg.AudioS16)
|
||||
|
||||
audioPlayer, err := ctx.NewPlayer(&mpegAudio{
|
||||
audio: mpg.Audio(),
|
||||
m: &p.m,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.audioPlayer = audioPlayer
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// updateFrame upadtes the current video frame.
|
||||
func (p *mpegPlayer) updateFrame() {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
|
||||
var pos float64
|
||||
if p.audioPlayer != nil {
|
||||
pos = p.audioPlayer.Position().Seconds()
|
||||
} else {
|
||||
if p.refTime != (time.Time{}) {
|
||||
pos = time.Since(p.refTime).Seconds()
|
||||
}
|
||||
}
|
||||
|
||||
video := p.mpg.Video()
|
||||
if video.HasEnded() {
|
||||
p.currentFrame.Clear()
|
||||
return
|
||||
}
|
||||
|
||||
d := 1 / p.mpg.Framerate()
|
||||
var mpegFrame *mpeg.Frame
|
||||
for video.Time()+d <= pos && !video.HasEnded() {
|
||||
mpegFrame = video.Decode()
|
||||
}
|
||||
|
||||
if mpegFrame == nil {
|
||||
return
|
||||
}
|
||||
p.currentFrame.WritePixels(mpegFrame.RGBA().Pix)
|
||||
}
|
||||
|
||||
// Draw draws the current frame onto the given screen.
|
||||
func (p *mpegPlayer) Draw(screen *ebiten.Image) {
|
||||
p.updateFrame()
|
||||
frame := p.currentFrame
|
||||
sw, sh := screen.Bounds().Dx(), screen.Bounds().Dy()
|
||||
fw, fh := frame.Bounds().Dx(), frame.Bounds().Dy()
|
||||
|
||||
op := ebiten.DrawImageOptions{}
|
||||
wf, hf := float64(sw)/float64(fw), float64(sh)/float64(fh)
|
||||
s := wf
|
||||
if hf < wf {
|
||||
s = hf
|
||||
}
|
||||
op.GeoM.Scale(s, s)
|
||||
|
||||
offsetX, offsetY := float64(screen.Bounds().Min.X), float64(screen.Bounds().Min.Y)
|
||||
op.GeoM.Translate(offsetX+(float64(sw)-float64(fw)*s)/2, offsetY+(float64(sh)-float64(fh)*s)/2)
|
||||
op.Filter = ebiten.FilterLinear
|
||||
|
||||
screen.DrawImage(frame, &op)
|
||||
}
|
||||
|
||||
// Play starts playing the video.
|
||||
func (p *mpegPlayer) Play() {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
|
||||
if p.mpg.HasEnded() {
|
||||
return
|
||||
}
|
||||
|
||||
if p.audioPlayer != nil {
|
||||
if p.audioPlayer.IsPlaying() {
|
||||
return
|
||||
}
|
||||
// Play refers (*mpegAudio).Read function, where the same mutex is used.
|
||||
// In order to avoid dead lock, use a different goroutine to start playing.
|
||||
// This issue happens especially on Windows where goroutines at Play are avoided in Oto (#1768).
|
||||
// TODO: Remove this hack in the future (ebitengine/oto#235).
|
||||
go p.audioPlayer.Play()
|
||||
return
|
||||
}
|
||||
|
||||
if p.refTime != (time.Time{}) {
|
||||
return
|
||||
}
|
||||
p.refTime = time.Now()
|
||||
}
|
||||
|
||||
type mpegAudio struct {
|
||||
audio *mpeg.Audio
|
||||
|
||||
// leftovers is the remaining audio samples of the previous Read call.
|
||||
leftovers []byte
|
||||
|
||||
// m is the mutex shared with the mpegPlayer.
|
||||
// As *mpeg.MPEG is not concurrent safe, this mutex is necessary.
|
||||
m *sync.Mutex
|
||||
}
|
||||
|
||||
func (a *mpegAudio) Read(buf []byte) (int, error) {
|
||||
a.m.Lock()
|
||||
defer a.m.Unlock()
|
||||
|
||||
var readBytes int
|
||||
if len(a.leftovers) > 0 {
|
||||
n := copy(buf, a.leftovers)
|
||||
readBytes += n
|
||||
buf = buf[n:]
|
||||
|
||||
copy(a.leftovers, a.leftovers[n:])
|
||||
a.leftovers = a.leftovers[:len(a.leftovers)-n]
|
||||
}
|
||||
|
||||
for len(buf) > 0 && !a.audio.HasEnded() {
|
||||
mpegSamples := a.audio.Decode()
|
||||
if mpegSamples == nil {
|
||||
break
|
||||
}
|
||||
|
||||
bs := make([]byte, len(mpegSamples.S16)*2)
|
||||
for i, s := range mpegSamples.S16 {
|
||||
bs[i*2] = byte(s)
|
||||
bs[i*2+1] = byte(s >> 8)
|
||||
}
|
||||
|
||||
n := copy(buf, bs)
|
||||
readBytes += n
|
||||
buf = buf[n:]
|
||||
|
||||
if n < len(bs) {
|
||||
a.leftovers = append(a.leftovers, bs[n:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if a.audio.HasEnded() {
|
||||
return readBytes, io.EOF
|
||||
}
|
||||
return readBytes, nil
|
||||
}
|
BIN
examples/video/shibuya.mpg
Normal file
BIN
examples/video/shibuya.mpg
Normal file
Binary file not shown.
BIN
examples/video/shibuya_noaudio.mpg
Normal file
BIN
examples/video/shibuya_noaudio.mpg
Normal file
Binary file not shown.
1
go.mod
1
go.mod
@ -22,6 +22,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gen2brain/mpeg v0.2.2 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -6,6 +6,8 @@ github.com/ebitengine/oto/v3 v3.3.0-alpha.1 h1:J2nBmQwPLKc4+yLObytq1jKNydI96l6Ej
|
||||
github.com/ebitengine/oto/v3 v3.3.0-alpha.1/go.mod h1:T2/VV0UWG97GEEf4kORMU2nCneYT/YmwSTxPutSVaUg=
|
||||
github.com/ebitengine/purego v0.8.0-alpha.1 h1:52AgJTNaQRi7YtOtdJl4hkxNWhAGMxuDuDjOVIp5Ojk=
|
||||
github.com/ebitengine/purego v0.8.0-alpha.1/go.mod h1:y8L+ZRLphbdPW2xs41fur/KaW57yTzrFsqsclHyHrTM=
|
||||
github.com/gen2brain/mpeg v0.2.2 h1:9VhRzbbTShC54H6LGl3r+K+zZHSfL7lauyYhlj090nw=
|
||||
github.com/gen2brain/mpeg v0.2.2/go.mod h1:i/ebyRRv/IoHixuZ9bElZnXbmfoUVPGQpdsJ4sVuX38=
|
||||
github.com/go-text/typesetting v0.1.1-0.20240402181327-ced1d6822703 h1:AqtMl9yw7r319Ah4W2afQm3Ql+PEsQKHds18tGvKhog=
|
||||
github.com/go-text/typesetting v0.1.1-0.20240402181327-ced1d6822703/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY=
|
||||
|
Loading…
Reference in New Issue
Block a user