diff --git a/examples/player-stream/bundle.py b/examples/player-stream/bundle.py new file mode 100644 index 0000000..b22e223 --- /dev/null +++ b/examples/player-stream/bundle.py @@ -0,0 +1,17 @@ +import os, fileinput, shutil + +bundleDirPath = os.path.abspath("bundle") +os.makedirs(bundleDirPath, exist_ok=True) + +for dllInfo in fileinput.input(): + dllInfo = dllInfo.strip() + dllInfoParts = dllInfo.split(sep=" ") + dllName = dllInfoParts[0] + dllPath = dllInfoParts[2] + dllBundlePath = os.path.join(bundleDirPath, dllName) + + if dllPath.startswith("/mingw64/bin"): + dllPath = os.path.join("C:/msys64", dllPath[1:]) + shutil.copyfile(dllPath, dllBundlePath) + +shutil.copyfile("player.exe", "bundle/player.exe") \ No newline at end of file diff --git a/examples/player-stream/demo.mkv b/examples/player-stream/demo.mkv new file mode 100644 index 0000000..d2e2983 Binary files /dev/null and b/examples/player-stream/demo.mkv differ diff --git a/examples/player-stream/demo.mp4 b/examples/player-stream/demo.mp4 new file mode 100755 index 0000000..d6f6f8a Binary files /dev/null and b/examples/player-stream/demo.mp4 differ diff --git a/examples/player-stream/main.go b/examples/player-stream/main.go new file mode 100755 index 0000000..d250c30 --- /dev/null +++ b/examples/player-stream/main.go @@ -0,0 +1,413 @@ +package main + +/* +#cgo pkg-config: libavformat libavcodec libavutil libswscale +#include +#include +#include +#include +#include +#include +*/ +import "C" +import ( + "bytes" + "encoding/binary" + "fmt" + "image" + "io" + "os" + "time" + "unsafe" + + "github.com/faiface/beep" + "github.com/faiface/beep/speaker" + "github.com/hajimehoshi/ebiten" + _ "github.com/silbinarywolf/preferdiscretegpu" + "github.com/zergon321/reisen" +) + +const ( + width = 1280 + height = 720 + frameBufferSize = 1024 + sampleRate = 44100 + channelCount = 2 + bitDepth = 8 + sampleBufferSize = 32 * channelCount * bitDepth * 1024 + SpeakerSampleRate beep.SampleRate = 44100 +) + +// readVideoAndAudio reads video and audio frames +// from the opened media and sends the decoded +// data to che channels to be played. +func 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() + + if err != nil { + return nil, nil, nil, err + } + + audioStream := media.AudioStreams()[0] + err = audioStream.Open() + + if err != nil { + return nil, nil, nil, err + } + + /*err = media.Streams()[0].Rewind(60 * time.Second) + + if err != nil { + return nil, nil, nil, err + }*/ + + /*err = media.Streams()[0].ApplyFilter("h264_mp4toannexb") + + if err != nil { + return nil, nil, nil, err + }*/ + + go func() { + for { + packet, gotPacket, err := media.ReadPacket() + + if err != nil { + continue + } + + if !gotPacket { + break + } + + /*hash := sha256.Sum256(packet.Data()) + fmt.Println(base58.Encode(hash[:]))*/ + + switch packet.Type() { + case reisen.StreamVideo: + s := media.Streams()[packet.StreamIndex()].(*reisen.VideoStream) + videoFrame, gotFrame, err := s.ReadVideoFrame() + + if err != nil { + continue + } + + 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 +} + +// streamSamples creates a new custom streamer for +// playing audio samples provided by the source channel. +// +// See https://github.com/faiface/beep/wiki/Making-own-streamers +// for reference. +func streamSamples(sampleSource <-chan [2]float64) beep.Streamer { + return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { + numRead := 0 + + for i := 0; i < len(samples); i++ { + sample, ok := <-sampleSource + + if !ok { + numRead = i + 1 + break + } + + samples[i] = sample + numRead++ + } + + if numRead < len(samples) { + return numRead, false + } + + return numRead, true + }) +} + +// Game holds all the data +// necessary for playing video. +type Game struct { + videoSprite *ebiten.Image + ticker <-chan time.Time + errs <-chan error + frameBuffer <-chan *image.RGBA + fps int + videoTotalFramesPlayed int + videoPlaybackFPS int + perSecond <-chan time.Time + last time.Time + deltaTime float64 +} + +// Strarts reading samples and frames +// of the media file. +func (game *Game) Start(fname string) error { + // Initialize the audio speaker. + err := speaker.Init(sampleRate, + SpeakerSampleRate.N(time.Second/10)) + + if err != nil { + return err + } + + // Sprite for drawing video frames. + game.videoSprite, err = ebiten.NewImage( + width, height, ebiten.FilterDefault) + + if err != nil { + return err + } + + file, err := os.Open("demo.mp4") + if err != nil { + return err + } + readseek := io.ReadSeeker(file) + handlers := &reisen.AVIOHandlers{} + handlers.ReadSeeker = &readseek + handlers.ReadPacket = func() ([]byte, int) { + + contents := make([]byte, reisen.IO_BUFFER_SIZE) + len, err := readseek.Read(contents) + if err != nil { + empty := make([]byte, 0) + return empty, C.AVERROR_EOF + } + return contents, len + } + handlers.Seek = func(offset int64, whence int) int64 { + if whence == 65536 { // avseek + return -1 + } + rs, err := readseek.Seek(offset, whence) + if err != nil { + fmt.Println(err) + return 1 + } + return rs + } + handlers.ReadFormat = func(media *reisen.Media) (*unsafe.Pointer, error) { + data := make([]byte, reisen.IO_BUFFER_SIZE) + readseek.Read(data) + readseek.Seek(0, io.SeekStart) + + res := C.CBytes(data) + emptyString := C.CString("") + + probeData := C.AVProbeData{} + probeData.buf = (*C.uint8_t)(res) + probeData.mime_type = emptyString + probeData.buf_size = C.int(reisen.IO_BUFFER_SIZE) + probeData.filename = emptyString + + format := C.av_probe_input_format(&probeData, 1) + result := unsafe.Pointer(format) + + C.free(res) + defer C.free(unsafe.Pointer(emptyString)) + return &result, nil + } + // Open the media file. + media, err := reisen.NewMediaFromReader("demo", handlers) + // media, err := reisen.NewMedia("demo.mp4") + + if err != nil { + return err + } + + // Get the FPS for playing + // video frames. + videoFPS, _ := media.Streams()[0].FrameRate() + + if err != nil { + return err + } + + // SPF for frame ticker. + spf := 1.0 / float64(videoFPS) + frameDuration, err := time. + ParseDuration(fmt.Sprintf("%fs", spf)) + + if err != nil { + return err + } + + // Start decoding streams. + var sampleSource <-chan [2]float64 + game.frameBuffer, sampleSource, + game.errs, err = readVideoAndAudio(media) + + if err != nil { + return err + } + + // Start playing audio samples. + speaker.Play(streamSamples(sampleSource)) + + game.ticker = time.Tick(frameDuration) + + // Setup metrics. + game.last = time.Now() + game.fps = 0 + game.perSecond = time.Tick(time.Second) + game.videoTotalFramesPlayed = 0 + game.videoPlaybackFPS = 0 + + return nil +} + +func (game *Game) Update(screen *ebiten.Image) error { + // Compute dt. + game.deltaTime = time.Since(game.last).Seconds() + game.last = time.Now() + + // Check for incoming errors. + select { + case err, ok := <-game.errs: + if ok { + return err + } + + default: + } + + // Read video frames and draw them. + select { + case <-game.ticker: + frame, ok := <-game.frameBuffer + + if ok { + game.videoSprite.ReplacePixels(frame.Pix) + + game.videoTotalFramesPlayed++ + game.videoPlaybackFPS++ + } + + default: + } + + // Draw the video sprite. + op := &ebiten.DrawImageOptions{} + err := screen.DrawImage(game.videoSprite, op) + + if err != nil { + return err + } + + game.fps++ + + // Update metrics in the window title. + select { + case <-game.perSecond: + ebiten.SetWindowTitle(fmt.Sprintf("%s | FPS: %d | dt: %f | Frames: %d | Video FPS: %d", + "Video", game.fps, game.deltaTime, game.videoTotalFramesPlayed, game.videoPlaybackFPS)) + + game.fps = 0 + game.videoPlaybackFPS = 0 + + default: + } + + return nil +} + +func (game *Game) Layout(a, b int) (int, int) { + return width, height +} + +func main() { + game := &Game{} + err := game.Start("demo.mp4") + handleError(err) + + ebiten.SetWindowSize(width, height) + ebiten.SetWindowTitle("Video") + err = ebiten.RunGame(game) + handleError(err) +} + +func handleError(err error) { + if err != nil { + panic(err) + } +} diff --git a/media.go b/media.go index edb8571..3024da5 100644 --- a/media.go +++ b/media.go @@ -1,23 +1,66 @@ package reisen -// #cgo pkg-config: libavformat libavcodec libavutil libswscale -// #include -// #include -// #include -// #include -// #include +/* +#cgo pkg-config: libavformat libavcodec libavutil libswscale +#include +#include +#include +#include +#include +#include +#include +#include + +static int setFormat(AVFormatContext *ctx, void *format) { + ctx->iformat = (AVInputFormat*)format; + return 0; +} + +extern int readCallBack(void*, uint8_t*, int); +extern int64_t seekCallBack(void*, int64_t, int); +extern int writeCallBack(void*, uint8_t*, int); + +*/ import "C" import ( + "bytes" "fmt" + "io" "time" "unsafe" ) +const ( + IO_BUFFER_SIZE = 50000000 + AVIO_FLAG_READ = 1 + AVIO_FLAG_WRITE = 2 + AVIO_FLAG_READ_WRITE = (AVIO_FLAG_READ | AVIO_FLAG_WRITE) +) + +var ( + handlersMap map[uintptr]*AVIOHandlers +) + +type AVIOHandlers struct { + ReadPacket func() ([]byte, int) + WritePacket func([]byte) int + Seek func(int64, int) int64 + ReadSeeker *io.ReadSeeker + ReadFormat func(*Media) (*unsafe.Pointer, error) +} + +type AVIOContext struct { + avAVIOContext *C.struct_AVIOContext + handlerKey uintptr +} + // Media is a media file containing // audio, video and other types of streams. type Media struct { ctx *C.AVFormatContext packet *C.AVPacket + mediaIO *AVIOContext + name string streams []Stream } @@ -232,13 +275,19 @@ func (media *Media) CloseDecode() error { func (media *Media) Close() { C.avformat_free_context(media.ctx) media.ctx = nil + if media.mediaIO != nil { + delete(handlersMap, media.mediaIO.handlerKey) + C.av_free(unsafe.Pointer(media.mediaIO.avAVIOContext.buffer)) + C.av_free(unsafe.Pointer(media.mediaIO.avAVIOContext)) + } } // NewMedia returns a new media container analyzer // for the specified media file. func NewMedia(filename string) (*Media, error) { media := &Media{ - ctx: C.avformat_alloc_context(), + ctx: C.avformat_alloc_context(), + name: filename, } if media.ctx == nil { @@ -263,3 +312,152 @@ func NewMedia(filename string) (*Media, error) { return media, nil } + +func getSize(stream io.Reader) int64 { + buf := new(bytes.Buffer) + buf.ReadFrom(stream) + return int64(buf.Len()) +} + +func NewMediaFromReader(name string, handlers *AVIOHandlers) (*Media, error) { + media := &Media{ + ctx: C.avformat_alloc_context(), + name: name, + } + + if media.ctx == nil { + return nil, fmt.Errorf( + "couldn't create a new media context") + } + + ioCtx, err := NewAVIOContext(media, handlers) + if err != nil { + fmt.Println(err) + panic("Error creating avio context") + } + media.mediaIO = ioCtx + + media.ctx.pb = ioCtx.avAVIOContext + media.ctx.flags |= C.AVFMT_FLAG_CUSTOM_IO + + tFormat, err := handlers.ReadFormat(media) + if err != nil { + return nil, fmt.Errorf( + "couldn't determine media format") + } + + C.setFormat(media.ctx, *tFormat) + + if media.ctx.iformat == nil { + return nil, fmt.Errorf( + "couldn't determine media format") + } + + status := C.avformat_open_input(&media.ctx, nil, nil, nil) + + if status < 0 { + return nil, fmt.Errorf( + "couldn't open file %s", name) + } + + err = media.findStreams() + + if err != nil { + return nil, err + } + return media, nil +} + +func NewAVIOContext(media *Media, handlers *AVIOHandlers) (*AVIOContext, error) { + result := &AVIOContext{} + + buffer := (*C.uchar)(C.av_malloc(C.size_t(IO_BUFFER_SIZE))) + + if buffer == nil { + return nil, fmt.Errorf("unable to allocate buffer") + } + + // we have to explicitly set it to nil, to force library using default handlers + var ptrRead, ptrWrite, ptrSeek *[0]byte = nil, nil, nil + + if handlers != nil { + if handlersMap == nil { + handlersMap = make(map[uintptr]*AVIOHandlers) + } + ptr := uintptr(unsafe.Pointer(media.ctx)) + handlersMap[ptr] = handlers + result.handlerKey = ptr + } + + flag := 0 + + if handlers.ReadPacket != nil { + ptrRead = (*[0]byte)(C.readCallBack) + } + + if handlers.WritePacket != nil { + ptrWrite = (*[0]byte)(C.writeCallBack) + flag = AVIO_FLAG_WRITE + } + + if handlers.Seek != nil { + ptrSeek = (*[0]byte)(C.seekCallBack) + } + + if handlers.ReadPacket != nil && handlers.WritePacket != nil { + flag = AVIO_FLAG_READ_WRITE + } + + if result.avAVIOContext = C.avio_alloc_context(buffer, C.int(IO_BUFFER_SIZE), C.int(flag), unsafe.Pointer(media.ctx), ptrRead, ptrWrite, ptrSeek); result.avAVIOContext == nil { + return nil, fmt.Errorf("unable to initialize avio context") + } + + return result, nil +} + +//export readCallBack +func readCallBack(opaque unsafe.Pointer, buf *C.uint8_t, buf_size C.int) C.int { + handlers, found := handlersMap[uintptr(opaque)] + if !found { + panic(fmt.Sprintf("No handlers instance found, according pointer: %v", opaque)) + } + + if handlers.ReadPacket == nil { + panic("No reader handler initialized") + } + + b, n := handlers.ReadPacket() + if n >= 0 { + C.memcpy(unsafe.Pointer(buf), unsafe.Pointer(&b[0]), C.size_t(n)) + } + + return C.int(n) +} + +//export writeCallBack +func writeCallBack(opaque unsafe.Pointer, buf *C.uint8_t, buf_size C.int) C.int { + handlers, found := handlersMap[uintptr(opaque)] + if !found { + panic(fmt.Sprintf("No handlers instance found, according pointer: %v", opaque)) + } + + if handlers.WritePacket == nil { + panic("No writer handler initialized.") + } + + return C.int(handlers.WritePacket(C.GoBytes(unsafe.Pointer(buf), buf_size))) +} + +//export seekCallBack +func seekCallBack(opaque unsafe.Pointer, offset C.int64_t, whence C.int) C.int64_t { + handlers, found := handlersMap[uintptr(opaque)] + if !found { + panic(fmt.Sprintf("No handlers instance found, according pointer: %v", opaque)) + } + + if handlers.Seek == nil { + panic("No seek handler initialized.") + } + + return C.int64_t(handlers.Seek(int64(offset), int(whence))) +}