├── .gitignore ├── LICENSE ├── README.md ├── audio.go ├── audioframe.go ├── errors.go ├── examples ├── analyze │ ├── bundle.py │ ├── demo.mkv │ ├── demo.mp4 │ └── main.go ├── player │ ├── Dockerfile │ ├── bundle.py │ ├── demo.mkv │ ├── demo.mp4 │ └── main.go └── rtmp │ ├── README.md │ ├── docker-compose.yml │ ├── main.go │ ├── nginx.conf │ ├── stream.sh │ └── video.mp4 ├── frame.go ├── go.mod ├── go.sum ├── interpolation.go ├── media.go ├── network.go ├── packet.go ├── pictures └── audio_sample_structure.png ├── platform_darwin.go ├── platform_linux.go ├── platform_windows.go ├── stream.go ├── time.go ├── unknown.go ├── video.go └── videoframe.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/player/bundle 2 | examples/analyze/bundle 3 | examples/player/player.exe 4 | examples/analyze/analyze.exe 5 | examples/player/main.exe 6 | examples/analyze/main.exe 7 | examples/player/player 8 | examples/analyze/analyze -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 NightGhost 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reisen [![GoDoc](https://godoc.org/github.com/zergon321/reisen?status.svg)](https://pkg.go.dev/github.com/zergon321/reisen) 2 | 3 | A simple library to extract video and audio frames from media containers (based on **libav**, i.e. **ffmpeg**). 4 | 5 | ## Dependencies 6 | 7 | The library requires **libav** components to work: 8 | 9 | - **libavformat** 10 | - **libavcodec** 11 | - **libavutil** 12 | - **libswresample** 13 | - **libswscale** 14 | 15 | For **Arch**-based **Linux** distributions: 16 | 17 | ```bash 18 | sudo pacman -S ffmpeg 19 | ``` 20 | 21 | For **Debian**-based **Linux** distributions: 22 | 23 | ```bash 24 | sudo add-apt-repository ppa:savoury1/ffmpeg4 25 | sudo apt install libswscale-dev libavcodec-dev libavformat-dev libswresample-dev libavutil-dev 26 | ``` 27 | 28 | For **macOS**: 29 | 30 | ```bash 31 | brew install ffmpeg 32 | ``` 33 | 34 | For **Windows** see the [detailed tutorial](https://medium.com/@maximgradan/how-to-easily-bundle-your-cgo-application-for-windows-8515d2b19f1e). 35 | 36 | ## Installation 37 | 38 | Just casually run this command: 39 | 40 | ```bash 41 | go get github.com/zergon321/reisen 42 | ``` 43 | 44 | ## Usage 45 | 46 | Any media file is composed of streams containing media data, e.g. audio, video and subtitles. The whole presentation data of the file is divided into packets. Each packet belongs to one of the streams and represents a single frame of its data. The process of decoding implies reading packets and decoding them into either video frames or audio frames. 47 | 48 | The library provides read video frames as **RGBA** pictures. The audio samples are provided as raw byte slices in the format of `AV_SAMPLE_FMT_DBL` (i.e. 8 bytes per sample for one channel, the data type is `float64`). The channel layout is stereo (2 channels). The byte order is little-endian. The detailed scheme of the audio samples sequence is given below. 49 | 50 | ![Audio sample structure](https://github.com/zergon321/reisen/blob/master/pictures/audio_sample_structure.png) 51 | 52 | You are welcome to look at the [examples](https://github.com/zergon321/reisen/tree/master/examples) to understand how to work with the library. Also please take a look at the detailed [tutorial](https://medium.com/@maximgradan/playing-videos-with-golang-83e67447b111). 53 | -------------------------------------------------------------------------------- /audio.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavformat libavcodec libavutil libswresample 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import ( 10 | "fmt" 11 | "unsafe" 12 | ) 13 | 14 | const ( 15 | // StandardChannelCount is used for 16 | // audio conversion while decoding 17 | // audio frames. 18 | StandardChannelCount = 2 19 | ) 20 | 21 | // AudioStream is a stream containing 22 | // audio frames consisting of audio samples. 23 | type AudioStream struct { 24 | baseStream 25 | swrCtx *C.SwrContext 26 | buffer *C.uint8_t 27 | bufferSize C.int 28 | } 29 | 30 | // ChannelCount returns the number of channels 31 | // (1 for mono, 2 for stereo, etc.). 32 | func (audio *AudioStream) ChannelCount() int { 33 | return int(audio.codecParams.channels) 34 | } 35 | 36 | // SampleRate returns the sample rate of the 37 | // audio stream. 38 | func (audio *AudioStream) SampleRate() int { 39 | return int(audio.codecParams.sample_rate) 40 | } 41 | 42 | // FrameSize returns the number of samples 43 | // contained in one frame of the audio. 44 | func (audio *AudioStream) FrameSize() int { 45 | return int(audio.codecParams.frame_size) 46 | } 47 | 48 | // Open opens the audio stream to decode 49 | // audio frames and samples from it. 50 | func (audio *AudioStream) Open() error { 51 | err := audio.open() 52 | 53 | if err != nil { 54 | return err 55 | } 56 | 57 | audio.swrCtx = C.swr_alloc_set_opts(nil, 58 | C.AV_CH_FRONT_LEFT|C.AV_CH_FRONT_RIGHT, 59 | C.AV_SAMPLE_FMT_DBL, audio.codecCtx.sample_rate, 60 | channelLayout(audio), audio. 61 | codecCtx.sample_fmt, audio.codecCtx. 62 | sample_rate, 0, nil) 63 | 64 | if audio.swrCtx == nil { 65 | return fmt.Errorf( 66 | "couldn't allocate an SWR context") 67 | } 68 | 69 | status := C.swr_init(audio.swrCtx) 70 | 71 | if status < 0 { 72 | return fmt.Errorf( 73 | "%d: couldn't initialize the SWR context", status) 74 | } 75 | 76 | audio.buffer = nil 77 | 78 | return nil 79 | } 80 | 81 | // ReadFrame reads a new frame from the stream. 82 | func (audio *AudioStream) ReadFrame() (Frame, bool, error) { 83 | return audio.ReadAudioFrame() 84 | } 85 | 86 | // ReadAudioFrame reads a new audio frame from the stream. 87 | func (audio *AudioStream) ReadAudioFrame() (*AudioFrame, bool, error) { 88 | ok, err := audio.read() 89 | 90 | if err != nil { 91 | return nil, false, err 92 | } 93 | 94 | if ok && audio.skip { 95 | return nil, true, nil 96 | } 97 | 98 | // No more data. 99 | if !ok { 100 | return nil, false, nil 101 | } 102 | 103 | maxBufferSize := C.av_samples_get_buffer_size( 104 | nil, StandardChannelCount, 105 | audio.frame.nb_samples, 106 | C.AV_SAMPLE_FMT_DBL, 1) 107 | 108 | if maxBufferSize < 0 { 109 | return nil, false, fmt.Errorf( 110 | "%d: couldn't get the max buffer size", maxBufferSize) 111 | } 112 | 113 | if maxBufferSize > audio.bufferSize { 114 | C.av_free(unsafe.Pointer(audio.buffer)) 115 | audio.buffer = nil 116 | } 117 | 118 | if audio.buffer == nil { 119 | audio.buffer = (*C.uint8_t)(unsafe.Pointer( 120 | C.av_malloc(bufferSize(maxBufferSize)))) 121 | audio.bufferSize = maxBufferSize 122 | 123 | if audio.buffer == nil { 124 | return nil, false, fmt.Errorf( 125 | "couldn't allocate an AV buffer") 126 | } 127 | } 128 | 129 | gotSamples := C.swr_convert(audio.swrCtx, 130 | &audio.buffer, audio.frame.nb_samples, 131 | &audio.frame.data[0], audio.frame.nb_samples) 132 | 133 | if gotSamples < 0 { 134 | return nil, false, fmt.Errorf( 135 | "%d: couldn't convert the audio frame", gotSamples) 136 | } 137 | 138 | data := C.GoBytes(unsafe.Pointer( 139 | audio.buffer), maxBufferSize) 140 | frame := newAudioFrame(audio, 141 | int64(audio.frame.pts), 142 | int(audio.frame.coded_picture_number), 143 | int(audio.frame.display_picture_number), data) 144 | 145 | return frame, true, nil 146 | } 147 | 148 | // Close closes the audio stream and 149 | // stops decoding audio frames. 150 | func (audio *AudioStream) Close() error { 151 | err := audio.close() 152 | 153 | if err != nil { 154 | return err 155 | } 156 | 157 | C.av_free(unsafe.Pointer(audio.buffer)) 158 | audio.buffer = nil 159 | C.swr_free(&audio.swrCtx) 160 | audio.swrCtx = nil 161 | 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /audioframe.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | // #include 10 | import "C" 11 | 12 | // AudioFrame is a data frame 13 | // obtained from an audio stream. 14 | type AudioFrame struct { 15 | baseFrame 16 | data []byte 17 | } 18 | 19 | // Data returns a raw slice of 20 | // audio frame samples. 21 | func (frame *AudioFrame) Data() []byte { 22 | return frame.data 23 | } 24 | 25 | // newAudioFrame returns a newly created audio frame. 26 | func newAudioFrame(stream Stream, pts int64, indCoded, indDisplay int, data []byte) *AudioFrame { 27 | frame := new(AudioFrame) 28 | 29 | frame.stream = stream 30 | frame.pts = pts 31 | frame.data = data 32 | frame.indexCoded = indCoded 33 | frame.indexDisplay = indDisplay 34 | 35 | return frame 36 | } 37 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | type ErrorType int 4 | 5 | const ( 6 | // ErrorAgain is returned when 7 | // the decoder needs more data 8 | // to serve the frame. 9 | ErrorAgain ErrorType = -11 10 | // ErrorInvalidValue is returned 11 | // when the function call argument 12 | // is invalid. 13 | ErrorInvalidValue ErrorType = -22 14 | // ErrorEndOfFile is returned upon 15 | // reaching the end of the media file. 16 | ErrorEndOfFile ErrorType = -541478725 17 | ) 18 | -------------------------------------------------------------------------------- /examples/analyze/bundle.py: -------------------------------------------------------------------------------- 1 | import os, fileinput, shutil 2 | 3 | bundleDirPath = os.path.abspath("bundle") 4 | os.makedirs(bundleDirPath, exist_ok=True) 5 | 6 | for dllInfo in fileinput.input(): 7 | dllInfo = dllInfo.strip() 8 | dllInfoParts = dllInfo.split(sep=" ") 9 | dllName = dllInfoParts[0] 10 | dllPath = dllInfoParts[2] 11 | dllBundlePath = os.path.join(bundleDirPath, dllName) 12 | 13 | if dllPath.startswith("/mingw64/bin"): 14 | dllPath = os.path.join("C:/msys64", dllPath[1:]) 15 | shutil.copyfile(dllPath, dllBundlePath) 16 | 17 | shutil.copyfile("analyze.exe", "bundle/analyze.exe") -------------------------------------------------------------------------------- /examples/analyze/demo.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/examples/analyze/demo.mkv -------------------------------------------------------------------------------- /examples/analyze/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/examples/analyze/demo.mp4 -------------------------------------------------------------------------------- /examples/analyze/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zergon321/reisen" 7 | ) 8 | 9 | func main() { 10 | // Open the media file by its name. 11 | media, err := reisen.NewMedia("demo.mp4") 12 | handleError(err) 13 | defer media.Close() 14 | dur, err := media.Duration() 15 | handleError(err) 16 | 17 | // Print the media properties. 18 | fmt.Println("Duration:", dur) 19 | fmt.Println("Format name:", media.FormatName()) 20 | fmt.Println("Format long name:", media.FormatLongName()) 21 | fmt.Println("MIME type:", media.FormatMIMEType()) 22 | fmt.Println("Number of streams:", media.StreamCount()) 23 | fmt.Println() 24 | 25 | // Enumerate the media file streams. 26 | for _, stream := range media.Streams() { 27 | dur, err := stream.Duration() 28 | handleError(err) 29 | tbNum, tbDen := stream.TimeBase() 30 | fpsNum, fpsDen := stream.FrameRate() 31 | 32 | // Print the properties common 33 | // for both stream types. 34 | fmt.Println("Index:", stream.Index()) 35 | fmt.Println("Stream type:", stream.Type()) 36 | fmt.Println("Codec name:", stream.CodecName()) 37 | fmt.Println("Codec long name:", stream.CodecLongName()) 38 | fmt.Println("Stream duration:", dur) 39 | fmt.Println("Stream bit rate:", stream.BitRate()) 40 | fmt.Printf("Time base: %d/%d\n", tbNum, tbDen) 41 | fmt.Printf("Frame rate: %d/%d\n", fpsNum, fpsDen) 42 | fmt.Println("Frame count:", stream.FrameCount()) 43 | fmt.Println() 44 | } 45 | 46 | // Do decoding. 47 | err = media.OpenDecode() 48 | handleError(err) 49 | gotPacket := true 50 | 51 | for i := 0; i < 9 && gotPacket; i++ { 52 | // Read packets one by one. A packet 53 | // can contain either a video frame 54 | // or an audio frame. 55 | var pkt *reisen.Packet 56 | pkt, gotPacket, err = media.ReadPacket() 57 | handleError(err) 58 | 59 | // Check if the media file 60 | // is depleted. 61 | if !gotPacket { 62 | break 63 | } 64 | 65 | // Determine what stream 66 | // the packet belongs to. 67 | switch pkt.Type() { 68 | case reisen.StreamVideo: 69 | s := media.Streams()[pkt.StreamIndex()].(*reisen.VideoStream) 70 | 71 | if !s.Opened() { 72 | err = s.Open() 73 | handleError(err) 74 | } 75 | 76 | videoFrame, gotFrame, err := s.ReadVideoFrame() 77 | handleError(err) 78 | 79 | // If the media file is 80 | // depleted. 81 | if !gotFrame { 82 | break 83 | } 84 | 85 | // If the packet doesn't 86 | // contain a whole frame, 87 | // just skip it. 88 | if videoFrame == nil { 89 | continue 90 | } 91 | 92 | pts, err := videoFrame.PresentationOffset() 93 | handleError(err) 94 | 95 | fmt.Println("Presentation duration offset:", pts) 96 | fmt.Println("Number of pixels:", len(videoFrame.Image().Pix)) 97 | fmt.Println("Coded picture number:", videoFrame.IndexCoded()) 98 | fmt.Println("Display picture number:", videoFrame.IndexDisplay()) 99 | fmt.Println() 100 | 101 | case reisen.StreamAudio: 102 | s := media.Streams()[pkt.StreamIndex()].(*reisen.AudioStream) 103 | 104 | if !s.Opened() { 105 | err = s.Open() 106 | handleError(err) 107 | } 108 | 109 | audioFrame, gotFrame, err := s.ReadAudioFrame() 110 | handleError(err) 111 | 112 | if !gotFrame { 113 | break 114 | } 115 | 116 | if audioFrame == nil { 117 | continue 118 | } 119 | 120 | pts, err := audioFrame.PresentationOffset() 121 | handleError(err) 122 | 123 | fmt.Println("Presentation duration offset:", pts) 124 | fmt.Println("Data length:", len(audioFrame.Data())) 125 | fmt.Println("Coded picture number:", audioFrame.IndexCoded()) 126 | fmt.Println("Display picture number:", audioFrame.IndexDisplay()) 127 | fmt.Println() 128 | } 129 | } 130 | 131 | for _, stream := range media.Streams() { 132 | err = stream.Close() 133 | handleError(err) 134 | } 135 | 136 | err = media.CloseDecode() 137 | handleError(err) 138 | } 139 | 140 | func handleError(err error) { 141 | if err != nil { 142 | panic(err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /examples/player/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as the base image 2 | FROM golang:1.20.5-alpine3.18 AS builder 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Install necessary packages 8 | RUN apk add ffmpeg 9 | 10 | # Copy the Go module files 11 | COPY go.mod go.sum ./ 12 | 13 | # Download and cache the Go module dependencies 14 | RUN go mod download 15 | 16 | # Copy the source code into the container 17 | COPY . . 18 | 19 | # Build the Go application 20 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o vidstreamsplit . -------------------------------------------------------------------------------- /examples/player/bundle.py: -------------------------------------------------------------------------------- 1 | import os, fileinput, shutil 2 | 3 | bundleDirPath = os.path.abspath("bundle") 4 | os.makedirs(bundleDirPath, exist_ok=True) 5 | 6 | for dllInfo in fileinput.input(): 7 | dllInfo = dllInfo.strip() 8 | dllInfoParts = dllInfo.split(sep=" ") 9 | dllName = dllInfoParts[0] 10 | dllPath = dllInfoParts[2] 11 | dllBundlePath = os.path.join(bundleDirPath, dllName) 12 | 13 | if dllPath.startswith("/mingw64/bin"): 14 | dllPath = os.path.join("C:/msys64", dllPath[1:]) 15 | shutil.copyfile(dllPath, dllBundlePath) 16 | 17 | shutil.copyfile("player.exe", "bundle/player.exe") -------------------------------------------------------------------------------- /examples/player/demo.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/examples/player/demo.mkv -------------------------------------------------------------------------------- /examples/player/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/examples/player/demo.mp4 -------------------------------------------------------------------------------- /examples/player/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "image" 8 | "time" 9 | 10 | "github.com/faiface/beep" 11 | "github.com/faiface/beep/speaker" 12 | "github.com/hajimehoshi/ebiten" 13 | _ "github.com/silbinarywolf/preferdiscretegpu" 14 | "github.com/zergon321/reisen" 15 | ) 16 | 17 | const ( 18 | width = 1280 19 | height = 720 20 | frameBufferSize = 1024 21 | sampleRate = 44100 22 | channelCount = 2 23 | bitDepth = 8 24 | sampleBufferSize = 32 * channelCount * bitDepth * 1024 25 | SpeakerSampleRate beep.SampleRate = 44100 26 | ) 27 | 28 | // readVideoAndAudio reads video and audio frames 29 | // from the opened media and sends the decoded 30 | // data to che channels to be played. 31 | func readVideoAndAudio(media *reisen.Media) (<-chan *image.RGBA, <-chan [2]float64, chan error, error) { 32 | frameBuffer := make(chan *image.RGBA, 33 | frameBufferSize) 34 | sampleBuffer := make(chan [2]float64, sampleBufferSize) 35 | errs := make(chan error) 36 | 37 | err := media.OpenDecode() 38 | 39 | if err != nil { 40 | return nil, nil, nil, err 41 | } 42 | 43 | videoStream := media.VideoStreams()[0] 44 | err = videoStream.Open() 45 | 46 | if err != nil { 47 | return nil, nil, nil, err 48 | } 49 | 50 | audioStream := media.AudioStreams()[0] 51 | err = audioStream.Open() 52 | 53 | if err != nil { 54 | return nil, nil, nil, err 55 | } 56 | 57 | /*err = media.Streams()[0].Rewind(60 * time.Second) 58 | 59 | if err != nil { 60 | return nil, nil, nil, err 61 | }*/ 62 | 63 | /*err = media.Streams()[0].ApplyFilter("h264_mp4toannexb") 64 | 65 | if err != nil { 66 | return nil, nil, nil, err 67 | }*/ 68 | 69 | go func() { 70 | for { 71 | packet, gotPacket, err := media.ReadPacket() 72 | 73 | if err != nil { 74 | go func(err error) { 75 | errs <- err 76 | }(err) 77 | } 78 | 79 | if !gotPacket { 80 | break 81 | } 82 | 83 | /*hash := sha256.Sum256(packet.Data()) 84 | fmt.Println(base58.Encode(hash[:]))*/ 85 | 86 | switch packet.Type() { 87 | case reisen.StreamVideo: 88 | s := media.Streams()[packet.StreamIndex()].(*reisen.VideoStream) 89 | videoFrame, gotFrame, err := s.ReadVideoFrame() 90 | 91 | if err != nil { 92 | go func(err error) { 93 | errs <- err 94 | }(err) 95 | } 96 | 97 | if !gotFrame { 98 | break 99 | } 100 | 101 | if videoFrame == nil { 102 | continue 103 | } 104 | 105 | frameBuffer <- videoFrame.Image() 106 | 107 | case reisen.StreamAudio: 108 | s := media.Streams()[packet.StreamIndex()].(*reisen.AudioStream) 109 | audioFrame, gotFrame, err := s.ReadAudioFrame() 110 | 111 | if err != nil { 112 | go func(err error) { 113 | errs <- err 114 | }(err) 115 | } 116 | 117 | if !gotFrame { 118 | break 119 | } 120 | 121 | if audioFrame == nil { 122 | continue 123 | } 124 | 125 | // Turn the raw byte data into 126 | // audio samples of type [2]float64. 127 | reader := bytes.NewReader(audioFrame.Data()) 128 | 129 | // See the README.md file for 130 | // detailed scheme of the sample structure. 131 | for reader.Len() > 0 { 132 | sample := [2]float64{0, 0} 133 | var result float64 134 | err = binary.Read(reader, binary.LittleEndian, &result) 135 | 136 | if err != nil { 137 | go func(err error) { 138 | errs <- err 139 | }(err) 140 | } 141 | 142 | sample[0] = result 143 | 144 | err = binary.Read(reader, binary.LittleEndian, &result) 145 | 146 | if err != nil { 147 | go func(err error) { 148 | errs <- err 149 | }(err) 150 | } 151 | 152 | sample[1] = result 153 | sampleBuffer <- sample 154 | } 155 | } 156 | } 157 | 158 | videoStream.Close() 159 | audioStream.Close() 160 | media.CloseDecode() 161 | close(frameBuffer) 162 | close(sampleBuffer) 163 | close(errs) 164 | }() 165 | 166 | return frameBuffer, sampleBuffer, errs, nil 167 | } 168 | 169 | // streamSamples creates a new custom streamer for 170 | // playing audio samples provided by the source channel. 171 | // 172 | // See https://github.com/faiface/beep/wiki/Making-own-streamers 173 | // for reference. 174 | func streamSamples(sampleSource <-chan [2]float64) beep.Streamer { 175 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 176 | numRead := 0 177 | 178 | for i := 0; i < len(samples); i++ { 179 | sample, ok := <-sampleSource 180 | 181 | if !ok { 182 | numRead = i + 1 183 | break 184 | } 185 | 186 | samples[i] = sample 187 | numRead++ 188 | } 189 | 190 | if numRead < len(samples) { 191 | return numRead, false 192 | } 193 | 194 | return numRead, true 195 | }) 196 | } 197 | 198 | // Game holds all the data 199 | // necessary for playing video. 200 | type Game struct { 201 | videoSprite *ebiten.Image 202 | ticker <-chan time.Time 203 | errs <-chan error 204 | frameBuffer <-chan *image.RGBA 205 | fps int 206 | videoTotalFramesPlayed int 207 | videoPlaybackFPS int 208 | perSecond <-chan time.Time 209 | last time.Time 210 | deltaTime float64 211 | } 212 | 213 | // Strarts reading samples and frames 214 | // of the media file. 215 | func (game *Game) Start(fname string) error { 216 | // Initialize the audio speaker. 217 | err := speaker.Init(sampleRate, 218 | SpeakerSampleRate.N(time.Second/10)) 219 | 220 | if err != nil { 221 | return err 222 | } 223 | 224 | // Sprite for drawing video frames. 225 | game.videoSprite, err = ebiten.NewImage( 226 | width, height, ebiten.FilterDefault) 227 | 228 | if err != nil { 229 | return err 230 | } 231 | 232 | // Open the media file. 233 | media, err := reisen.NewMedia(fname) 234 | 235 | if err != nil { 236 | return err 237 | } 238 | 239 | // Get the FPS for playing 240 | // video frames. 241 | videoFPS, _ := media.Streams()[0].FrameRate() 242 | 243 | if err != nil { 244 | return err 245 | } 246 | 247 | // SPF for frame ticker. 248 | spf := 1.0 / float64(videoFPS) 249 | frameDuration, err := time. 250 | ParseDuration(fmt.Sprintf("%fs", spf)) 251 | 252 | if err != nil { 253 | return err 254 | } 255 | 256 | // Start decoding streams. 257 | var sampleSource <-chan [2]float64 258 | game.frameBuffer, sampleSource, 259 | game.errs, err = readVideoAndAudio(media) 260 | 261 | if err != nil { 262 | return err 263 | } 264 | 265 | // Start playing audio samples. 266 | speaker.Play(streamSamples(sampleSource)) 267 | 268 | game.ticker = time.Tick(frameDuration) 269 | 270 | // Setup metrics. 271 | game.last = time.Now() 272 | game.fps = 0 273 | game.perSecond = time.Tick(time.Second) 274 | game.videoTotalFramesPlayed = 0 275 | game.videoPlaybackFPS = 0 276 | 277 | return nil 278 | } 279 | 280 | func (game *Game) Update(screen *ebiten.Image) error { 281 | // Compute dt. 282 | game.deltaTime = time.Since(game.last).Seconds() 283 | game.last = time.Now() 284 | 285 | // Check for incoming errors. 286 | select { 287 | case err, ok := <-game.errs: 288 | if ok { 289 | return err 290 | } 291 | 292 | default: 293 | } 294 | 295 | // Read video frames and draw them. 296 | select { 297 | case <-game.ticker: 298 | frame, ok := <-game.frameBuffer 299 | 300 | if ok { 301 | game.videoSprite.ReplacePixels(frame.Pix) 302 | 303 | game.videoTotalFramesPlayed++ 304 | game.videoPlaybackFPS++ 305 | } 306 | 307 | default: 308 | } 309 | 310 | // Draw the video sprite. 311 | op := &ebiten.DrawImageOptions{} 312 | err := screen.DrawImage(game.videoSprite, op) 313 | 314 | if err != nil { 315 | return err 316 | } 317 | 318 | game.fps++ 319 | 320 | // Update metrics in the window title. 321 | select { 322 | case <-game.perSecond: 323 | ebiten.SetWindowTitle(fmt.Sprintf("%s | FPS: %d | dt: %f | Frames: %d | Video FPS: %d", 324 | "Video", game.fps, game.deltaTime, game.videoTotalFramesPlayed, game.videoPlaybackFPS)) 325 | 326 | game.fps = 0 327 | game.videoPlaybackFPS = 0 328 | 329 | default: 330 | } 331 | 332 | return nil 333 | } 334 | 335 | func (game *Game) Layout(a, b int) (int, int) { 336 | return width, height 337 | } 338 | 339 | func main() { 340 | game := &Game{} 341 | err := game.Start("demo.mp4") 342 | handleError(err) 343 | 344 | ebiten.SetWindowSize(width, height) 345 | ebiten.SetWindowTitle("Video") 346 | err = ebiten.RunGame(game) 347 | handleError(err) 348 | } 349 | 350 | func handleError(err error) { 351 | if err != nil { 352 | panic(err) 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /examples/rtmp/README.md: -------------------------------------------------------------------------------- 1 | # An RTMP stream reading example 2 | 3 | ## Dependencies 4 | 5 | - **Docker** 6 | - `docker-compose` 7 | 8 | ## How to launch 9 | 10 | First run `docker-compose up` to launch the **RTMP** streaming server. Then run `./stream.sh` to start streaming `video.mp4` over **RTMP**. After it execute `go run main.go` to watch the stream. -------------------------------------------------------------------------------- /examples/rtmp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | rtmp-streamer: 5 | image: tiangolo/nginx-rtmp 6 | volumes: 7 | - ./nginx.conf:/etc/nginx/nginx.conf 8 | ports: 9 | - 1935:1935 -------------------------------------------------------------------------------- /examples/rtmp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "image" 8 | "time" 9 | 10 | "github.com/faiface/beep" 11 | "github.com/faiface/beep/speaker" 12 | "github.com/hajimehoshi/ebiten" 13 | _ "github.com/silbinarywolf/preferdiscretegpu" 14 | "github.com/zergon321/reisen" 15 | ) 16 | 17 | const ( 18 | width = 1280 19 | height = 720 20 | frameBufferSize = 1024 21 | sampleRate = 44100 22 | channelCount = 2 23 | bitDepth = 8 24 | sampleBufferSize = 32 * channelCount * bitDepth * 1024 25 | SpeakerSampleRate beep.SampleRate = 44100 26 | ) 27 | 28 | // readVideoAndAudio reads video and audio frames 29 | // from the opened media and sends the decoded 30 | // data to che channels to be played. 31 | func readVideoAndAudio(media *reisen.Media) (<-chan *image.RGBA, <-chan [2]float64, chan error, error) { 32 | frameBuffer := make(chan *image.RGBA, 33 | frameBufferSize) 34 | sampleBuffer := make(chan [2]float64, sampleBufferSize) 35 | errs := make(chan error) 36 | 37 | err := media.OpenDecode() 38 | 39 | if err != nil { 40 | return nil, nil, nil, err 41 | } 42 | 43 | videoStream := media.VideoStreams()[0] 44 | err = videoStream.Open() 45 | 46 | if err != nil { 47 | return nil, nil, nil, err 48 | } 49 | 50 | audioStream := media.AudioStreams()[0] 51 | err = audioStream.Open() 52 | 53 | if err != nil { 54 | return nil, nil, nil, err 55 | } 56 | 57 | /*err = media.Streams()[0].Rewind(60 * time.Second) 58 | 59 | if err != nil { 60 | return nil, nil, nil, err 61 | }*/ 62 | 63 | /*err = media.Streams()[0].ApplyFilter("h264_mp4toannexb") 64 | 65 | if err != nil { 66 | return nil, nil, nil, err 67 | }*/ 68 | 69 | go func() { 70 | for { 71 | packet, gotPacket, err := media.ReadPacket() 72 | 73 | if err != nil { 74 | go func(err error) { 75 | errs <- err 76 | }(err) 77 | } 78 | 79 | if !gotPacket { 80 | break 81 | } 82 | 83 | /*hash := sha256.Sum256(packet.Data()) 84 | fmt.Println(base58.Encode(hash[:]))*/ 85 | 86 | switch packet.Type() { 87 | case reisen.StreamVideo: 88 | s := media.Streams()[packet.StreamIndex()].(*reisen.VideoStream) 89 | videoFrame, gotFrame, err := s.ReadVideoFrame() 90 | 91 | if err != nil { 92 | go func(err error) { 93 | errs <- err 94 | }(err) 95 | } 96 | 97 | if !gotFrame { 98 | break 99 | } 100 | 101 | if videoFrame == nil { 102 | continue 103 | } 104 | 105 | offset, err := videoFrame.PresentationOffset() 106 | fmt.Println("video frame offset:", offset, err) 107 | 108 | frameBuffer <- videoFrame.Image() 109 | 110 | case reisen.StreamAudio: 111 | s := media.Streams()[packet.StreamIndex()].(*reisen.AudioStream) 112 | audioFrame, gotFrame, err := s.ReadAudioFrame() 113 | 114 | if err != nil { 115 | go func(err error) { 116 | errs <- err 117 | }(err) 118 | } 119 | 120 | if !gotFrame { 121 | break 122 | } 123 | 124 | if audioFrame == nil { 125 | continue 126 | } 127 | 128 | offset, err := audioFrame.PresentationOffset() 129 | fmt.Println("audio frame offset:", offset, err) 130 | 131 | // Turn the raw byte data into 132 | // audio samples of type [2]float64. 133 | reader := bytes.NewReader(audioFrame.Data()) 134 | 135 | // See the README.md file for 136 | // detailed scheme of the sample structure. 137 | for reader.Len() > 0 { 138 | sample := [2]float64{0, 0} 139 | var result float64 140 | err = binary.Read(reader, binary.LittleEndian, &result) 141 | 142 | if err != nil { 143 | go func(err error) { 144 | errs <- err 145 | }(err) 146 | } 147 | 148 | sample[0] = result 149 | 150 | err = binary.Read(reader, binary.LittleEndian, &result) 151 | 152 | if err != nil { 153 | go func(err error) { 154 | errs <- err 155 | }(err) 156 | } 157 | 158 | sample[1] = result 159 | sampleBuffer <- sample 160 | } 161 | } 162 | } 163 | 164 | videoStream.Close() 165 | audioStream.Close() 166 | media.CloseDecode() 167 | close(frameBuffer) 168 | close(sampleBuffer) 169 | close(errs) 170 | }() 171 | 172 | return frameBuffer, sampleBuffer, errs, nil 173 | } 174 | 175 | // streamSamples creates a new custom streamer for 176 | // playing audio samples provided by the source channel. 177 | // 178 | // See https://github.com/faiface/beep/wiki/Making-own-streamers 179 | // for reference. 180 | func streamSamples(sampleSource <-chan [2]float64) beep.Streamer { 181 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 182 | numRead := 0 183 | 184 | for i := 0; i < len(samples); i++ { 185 | sample, ok := <-sampleSource 186 | 187 | if !ok { 188 | numRead = i + 1 189 | break 190 | } 191 | 192 | samples[i] = sample 193 | numRead++ 194 | } 195 | 196 | if numRead < len(samples) { 197 | return numRead, false 198 | } 199 | 200 | return numRead, true 201 | }) 202 | } 203 | 204 | // Game holds all the data 205 | // necessary for playing video. 206 | type Game struct { 207 | videoSprite *ebiten.Image 208 | ticker <-chan time.Time 209 | errs <-chan error 210 | frameBuffer <-chan *image.RGBA 211 | fps int 212 | videoTotalFramesPlayed int 213 | videoPlaybackFPS int 214 | perSecond <-chan time.Time 215 | last time.Time 216 | deltaTime float64 217 | } 218 | 219 | // Strarts reading samples and frames 220 | // of the media file. 221 | func (game *Game) Start(fname string) error { 222 | // For RTMP stream reading. 223 | err := reisen.NetworkInitialize() 224 | handleError(err) 225 | defer reisen.NetworkDeinitialize() 226 | 227 | // Initialize the audio speaker. 228 | err = speaker.Init(sampleRate, 229 | SpeakerSampleRate.N(time.Second/10)) 230 | 231 | if err != nil { 232 | return err 233 | } 234 | 235 | // Sprite for drawing video frames. 236 | game.videoSprite, err = ebiten.NewImage( 237 | width, height, ebiten.FilterDefault) 238 | 239 | if err != nil { 240 | return err 241 | } 242 | 243 | // Open the media file. 244 | media, err := reisen.NewMedia(fname) 245 | 246 | if err != nil { 247 | return err 248 | } 249 | 250 | // Get the FPS for playing 251 | // video frames. 252 | videoFPS, _ := media.VideoStreams()[0].FrameRate() 253 | 254 | if err != nil { 255 | return err 256 | } 257 | 258 | // SPF for frame ticker. 259 | spf := 1.0 / float64(videoFPS) 260 | frameDuration, err := time. 261 | ParseDuration(fmt.Sprintf("%fs", spf)) 262 | 263 | if err != nil { 264 | return err 265 | } 266 | 267 | // Start decoding streams. 268 | var sampleSource <-chan [2]float64 269 | game.frameBuffer, sampleSource, 270 | game.errs, err = readVideoAndAudio(media) 271 | 272 | if err != nil { 273 | return err 274 | } 275 | 276 | // Start playing audio samples. 277 | speaker.Play(streamSamples(sampleSource)) 278 | 279 | game.ticker = time.Tick(frameDuration) 280 | 281 | // Setup metrics. 282 | game.last = time.Now() 283 | game.fps = 0 284 | game.perSecond = time.Tick(time.Second) 285 | game.videoTotalFramesPlayed = 0 286 | game.videoPlaybackFPS = 0 287 | 288 | return nil 289 | } 290 | 291 | func (game *Game) Update(screen *ebiten.Image) error { 292 | // Compute dt. 293 | game.deltaTime = time.Since(game.last).Seconds() 294 | game.last = time.Now() 295 | 296 | // Check for incoming errors. 297 | select { 298 | case err, ok := <-game.errs: 299 | if ok { 300 | return err 301 | } 302 | 303 | default: 304 | } 305 | 306 | // Read video frames and draw them. 307 | select { 308 | case <-game.ticker: 309 | frame, ok := <-game.frameBuffer 310 | 311 | if ok { 312 | game.videoSprite.ReplacePixels(frame.Pix) 313 | 314 | game.videoTotalFramesPlayed++ 315 | game.videoPlaybackFPS++ 316 | } 317 | 318 | default: 319 | } 320 | 321 | // Draw the video sprite. 322 | op := &ebiten.DrawImageOptions{} 323 | err := screen.DrawImage(game.videoSprite, op) 324 | 325 | if err != nil { 326 | return err 327 | } 328 | 329 | game.fps++ 330 | 331 | // Update metrics in the window title. 332 | select { 333 | case <-game.perSecond: 334 | ebiten.SetWindowTitle(fmt.Sprintf("%s | FPS: %d | dt: %f | Frames: %d | Video FPS: %d", 335 | "Video", game.fps, game.deltaTime, game.videoTotalFramesPlayed, game.videoPlaybackFPS)) 336 | 337 | game.fps = 0 338 | game.videoPlaybackFPS = 0 339 | 340 | default: 341 | } 342 | 343 | return nil 344 | } 345 | 346 | func (game *Game) Layout(a, b int) (int, int) { 347 | return width, height 348 | } 349 | 350 | func main() { 351 | game := &Game{} 352 | err := game.Start("rtmp://127.0.0.1/live/stream") 353 | handleError(err) 354 | 355 | ebiten.SetWindowSize(width, height) 356 | ebiten.SetWindowTitle("Video") 357 | err = ebiten.RunGame(game) 358 | handleError(err) 359 | } 360 | 361 | func handleError(err error) { 362 | if err != nil { 363 | panic(err) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /examples/rtmp/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | rtmp_auto_push on; 3 | events {} 4 | 5 | rtmp { 6 | server { 7 | listen 1935; 8 | chunk_size 4096; 9 | allow publish 0.0.0.0; 10 | 11 | application live { 12 | live on; 13 | record off; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /examples/rtmp/stream.sh: -------------------------------------------------------------------------------- 1 | ffmpeg -re -i video.mp4 -c:v copy -c:a aac -ar 44100 -ac 1 -f flv rtmp://127.0.0.1/live/stream -------------------------------------------------------------------------------- /examples/rtmp/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/examples/rtmp/video.mp4 -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | // #include 10 | import "C" 11 | import ( 12 | "fmt" 13 | "time" 14 | ) 15 | 16 | // Frame is an abstract data frame. 17 | type Frame interface { 18 | Data() []byte 19 | PresentationOffset() (time.Duration, error) 20 | } 21 | 22 | // baseFrame contains the information 23 | // common for all frames of any type. 24 | type baseFrame struct { 25 | stream Stream 26 | pts int64 27 | indexCoded int 28 | indexDisplay int 29 | } 30 | 31 | // PresentationOffset returns the duration offset 32 | // since the start of the media at which the frame 33 | // should be played. 34 | func (frame *baseFrame) PresentationOffset() (time.Duration, error) { 35 | tbNum, tbDen := frame.stream.TimeBase() 36 | tb := float64(tbNum) / float64(tbDen) 37 | tm := float64(frame.pts) * tb 38 | 39 | return time.ParseDuration(fmt.Sprintf("%fs", tm)) 40 | } 41 | 42 | // IndexCoded returns the index of 43 | // the frame in the bitstream order. 44 | func (frame *baseFrame) IndexCoded() int { 45 | return frame.indexCoded 46 | } 47 | 48 | // IndexDisplay returns the index of 49 | // the frame in the display order. 50 | func (frame *baseFrame) IndexDisplay() int { 51 | return frame.indexDisplay 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zergon321/reisen 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/faiface/beep v1.0.2 7 | github.com/hajimehoshi/ebiten v1.12.12 8 | github.com/silbinarywolf/preferdiscretegpu v1.0.0 9 | ) 10 | 11 | require ( 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 // indirect 13 | github.com/hajimehoshi/oto v0.6.8 // indirect 14 | github.com/pkg/errors v0.8.1 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76 // indirect 17 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f // indirect 18 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ= 3 | github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM= 4 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 5 | github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ= 6 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 h1:Ac1OEHHkbAZ6EUnJahF0GKcU0FjPc/V8F1DvjhKngFE= 7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 8 | github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= 9 | github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 11 | github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 12 | github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 13 | github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4= 14 | github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= 15 | github.com/hajimehoshi/bitmapfont v1.3.0/go.mod h1:/Qb7yVjHYNUV4JdqNkPs6BSZwLjKqkZOMIp6jZD0KgE= 16 | github.com/hajimehoshi/ebiten v1.12.12 h1:JvmF1bXRa+t+/CcLWxrJCRsdjs2GyBYBSiFAfIqDFlI= 17 | github.com/hajimehoshi/ebiten v1.12.12/go.mod h1:1XI25ImVCDPJiXox4h9yK/CvN5sjDYnbF4oZcFzPXHw= 18 | github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 19 | github.com/hajimehoshi/go-mp3 v0.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw= 20 | github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 21 | github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04= 22 | github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM= 23 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 24 | github.com/hajimehoshi/oto v0.6.8 h1:yRb3EJQ4lAkBgZYheqmdH6Lr77RV9nSWFsK/jwWdTNY= 25 | github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 26 | github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 27 | github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= 28 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= 29 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= 33 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 34 | github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= 35 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 36 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 37 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 38 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/silbinarywolf/preferdiscretegpu v1.0.0 h1:tuvXLRCnoFMFyk74/8PFvO0B5rjDmXm0JgNTaOYAHT0= 40 | github.com/silbinarywolf/preferdiscretegpu v1.0.0/go.mod h1:h3s2GkfAP2sWqoS7v/PxAlFOQ1azMRsZxUJNw47QhLc= 41 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 46 | golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 47 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 48 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 49 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 50 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 51 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 52 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 53 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 54 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= 55 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 56 | golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 57 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 58 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 59 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f h1:aEcjdTsycgPqO/caTgnxfR9xwWOltP/21vtJyFztEy0= 60 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 61 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 62 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 63 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 65 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 66 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 67 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 68 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= 78 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 80 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 81 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 82 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 83 | golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 84 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= 88 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | -------------------------------------------------------------------------------- /interpolation.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libswscale 4 | // #include 5 | import "C" 6 | 7 | // InterpolationAlgorithm is used when 8 | // we scale a video frame in a different resolution. 9 | type InterpolationAlgorithm int 10 | 11 | const ( 12 | InterpolationFastBilinear InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_FAST_BILINEAR) 13 | InterpolationBilinear InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_BILINEAR) 14 | InterpolationBicubic InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_BICUBIC) 15 | InterpolationX InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_X) 16 | InterpolationPoint InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_POINT) 17 | InterpolationArea InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_AREA) 18 | InterpolationBicubicBilinear InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_BICUBLIN) 19 | InterpolationGauss InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_GAUSS) 20 | InterpolationSinc InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_SINC) 21 | InterpolationLanczos InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_LANCZOS) 22 | InterpolationSpline InterpolationAlgorithm = InterpolationAlgorithm(C.SWS_SPLINE) 23 | ) 24 | 25 | // String returns the name of the interpolation algorithm. 26 | func (interpolationAlg InterpolationAlgorithm) String() string { 27 | switch interpolationAlg { 28 | case InterpolationFastBilinear: 29 | return "fast bilinear" 30 | 31 | case InterpolationBilinear: 32 | return "bilinear" 33 | 34 | case InterpolationBicubic: 35 | return "bicubic" 36 | 37 | case InterpolationX: 38 | return "x" 39 | 40 | case InterpolationPoint: 41 | return "point" 42 | 43 | case InterpolationArea: 44 | return "area" 45 | 46 | case InterpolationBicubicBilinear: 47 | return "bicubic bilinear" 48 | 49 | case InterpolationSinc: 50 | return "sinc" 51 | 52 | case InterpolationLanczos: 53 | return "lanczos" 54 | 55 | case InterpolationSpline: 56 | return "spline" 57 | 58 | default: 59 | return "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /media.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavformat libavcodec libavutil libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | import "C" 10 | 11 | import ( 12 | "fmt" 13 | "time" 14 | "unsafe" 15 | ) 16 | 17 | // Media is a media file containing 18 | // audio, video and other types of streams. 19 | type Media struct { 20 | ctx *C.AVFormatContext 21 | packet *C.AVPacket 22 | streams []Stream 23 | } 24 | 25 | // StreamCount returns the number of streams. 26 | func (media *Media) StreamCount() int { 27 | return int(media.ctx.nb_streams) 28 | } 29 | 30 | // Streams returns a slice of all the available 31 | // media data streams. 32 | func (media *Media) Streams() []Stream { 33 | streams := make([]Stream, len(media.streams)) 34 | copy(streams, media.streams) 35 | 36 | return streams 37 | } 38 | 39 | // VideoStreams returns all the 40 | // video streams of the media file. 41 | func (media *Media) VideoStreams() []*VideoStream { 42 | videoStreams := []*VideoStream{} 43 | 44 | for _, stream := range media.streams { 45 | if videoStream, ok := stream.(*VideoStream); ok { 46 | videoStreams = append(videoStreams, videoStream) 47 | } 48 | } 49 | 50 | return videoStreams 51 | } 52 | 53 | // AudioStreams returns all the 54 | // audio streams of the media file. 55 | func (media *Media) AudioStreams() []*AudioStream { 56 | audioStreams := []*AudioStream{} 57 | 58 | for _, stream := range media.streams { 59 | if audioStream, ok := stream.(*AudioStream); ok { 60 | audioStreams = append(audioStreams, audioStream) 61 | } 62 | } 63 | 64 | return audioStreams 65 | } 66 | 67 | // Duration returns the overall duration 68 | // of the media file. 69 | func (media *Media) Duration() (time.Duration, error) { 70 | dur := media.ctx.duration 71 | tm := float64(dur) / float64(TimeBase) 72 | 73 | return time.ParseDuration(fmt.Sprintf("%fs", tm)) 74 | } 75 | 76 | // FormatName returns the name of the media format. 77 | func (media *Media) FormatName() string { 78 | if media.ctx.iformat.name == nil { 79 | return "" 80 | } 81 | 82 | return C.GoString(media.ctx.iformat.name) 83 | } 84 | 85 | // FormatLongName returns the long name 86 | // of the media container. 87 | func (media *Media) FormatLongName() string { 88 | if media.ctx.iformat.long_name == nil { 89 | return "" 90 | } 91 | 92 | return C.GoString(media.ctx.iformat.long_name) 93 | } 94 | 95 | // FormatMIMEType returns the MIME type name 96 | // of the media container. 97 | func (media *Media) FormatMIMEType() string { 98 | if media.ctx.iformat.mime_type == nil { 99 | return "" 100 | } 101 | 102 | return C.GoString(media.ctx.iformat.mime_type) 103 | } 104 | 105 | // findStreams retrieves the stream information 106 | // from the media container. 107 | func (media *Media) findStreams() error { 108 | streams := []Stream{} 109 | status := C.avformat_find_stream_info(media.ctx, nil) 110 | 111 | if status < 0 { 112 | return fmt.Errorf( 113 | "couldn't find stream information") 114 | } 115 | 116 | innerStreams := unsafe.Slice( 117 | media.ctx.streams, media.ctx.nb_streams) 118 | 119 | for _, innerStream := range innerStreams { 120 | codecParams := innerStream.codecpar 121 | codec := C.avcodec_find_decoder(codecParams.codec_id) 122 | 123 | if codec == nil { 124 | unknownStream := new(UnknownStream) 125 | unknownStream.inner = innerStream 126 | unknownStream.codecParams = codecParams 127 | unknownStream.media = media 128 | 129 | streams = append(streams, unknownStream) 130 | 131 | continue 132 | } 133 | 134 | switch codecParams.codec_type { 135 | case C.AVMEDIA_TYPE_VIDEO: 136 | videoStream := new(VideoStream) 137 | videoStream.inner = innerStream 138 | videoStream.codecParams = codecParams 139 | videoStream.codec = codec 140 | videoStream.media = media 141 | 142 | streams = append(streams, videoStream) 143 | 144 | case C.AVMEDIA_TYPE_AUDIO: 145 | audioStream := new(AudioStream) 146 | audioStream.inner = innerStream 147 | audioStream.codecParams = codecParams 148 | audioStream.codec = codec 149 | audioStream.media = media 150 | 151 | streams = append(streams, audioStream) 152 | 153 | default: 154 | unknownStream := new(UnknownStream) 155 | unknownStream.inner = innerStream 156 | unknownStream.codecParams = codecParams 157 | unknownStream.codec = codec 158 | unknownStream.media = media 159 | 160 | streams = append(streams, unknownStream) 161 | } 162 | } 163 | 164 | media.streams = streams 165 | 166 | return nil 167 | } 168 | 169 | // OpenDecode opens the media container for decoding. 170 | // 171 | // CloseDecode() should be called afterwards. 172 | func (media *Media) OpenDecode() error { 173 | media.packet = C.av_packet_alloc() 174 | 175 | if media.packet == nil { 176 | return fmt.Errorf( 177 | "couldn't allocate a new packet") 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // ReadPacket reads the next packet from the media stream. 184 | func (media *Media) ReadPacket() (*Packet, bool, error) { 185 | status := C.av_read_frame(media.ctx, media.packet) 186 | 187 | if status < 0 { 188 | if status == C.int(ErrorAgain) { 189 | return nil, true, nil 190 | } 191 | 192 | // No packets anymore. 193 | return nil, false, nil 194 | } 195 | 196 | // Filter the packet if needed. 197 | packetStream := media.streams[media.packet.stream_index] 198 | outPacket := media.packet 199 | 200 | if packetStream.filter() != nil { 201 | filter := packetStream.filter() 202 | packetIn := packetStream.filterIn() 203 | packetOut := packetStream.filterOut() 204 | 205 | status = C.av_packet_ref(packetIn, media.packet) 206 | 207 | if status < 0 { 208 | return nil, false, 209 | fmt.Errorf("%d: couldn't reference the packet", 210 | status) 211 | } 212 | 213 | status = C.av_bsf_send_packet(filter, packetIn) 214 | 215 | if status < 0 { 216 | return nil, false, 217 | fmt.Errorf("%d: couldn't send the packet to the filter", 218 | status) 219 | } 220 | 221 | status = C.av_bsf_receive_packet(filter, packetOut) 222 | 223 | if status < 0 { 224 | return nil, false, 225 | fmt.Errorf("%d: couldn't receive the packet from the filter", 226 | status) 227 | } 228 | 229 | outPacket = packetOut 230 | } 231 | 232 | return newPacket(media, outPacket), true, nil 233 | } 234 | 235 | // CloseDecode closes the media container for decoding. 236 | func (media *Media) CloseDecode() error { 237 | C.av_free(unsafe.Pointer(media.packet)) 238 | media.packet = nil 239 | 240 | return nil 241 | } 242 | 243 | // Close closes the media container. 244 | func (media *Media) Close() { 245 | C.avformat_free_context(media.ctx) 246 | media.ctx = nil 247 | } 248 | 249 | // NewMedia returns a new media container analyzer 250 | // for the specified media file. 251 | func NewMedia(filename string) (*Media, error) { 252 | media := &Media{ 253 | ctx: C.avformat_alloc_context(), 254 | } 255 | 256 | if media.ctx == nil { 257 | return nil, fmt.Errorf( 258 | "couldn't create a new media context") 259 | } 260 | 261 | fname := C.CString(filename) 262 | status := C.avformat_open_input(&media.ctx, fname, nil, nil) 263 | 264 | if status < 0 { 265 | return nil, fmt.Errorf( 266 | "couldn't open file %s", filename) 267 | } 268 | 269 | C.free(unsafe.Pointer(fname)) 270 | err := media.findStreams() 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | return media, nil 276 | } 277 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #include 4 | import "C" 5 | import "fmt" 6 | 7 | func NetworkInitialize() error { 8 | code := C.avformat_network_init() 9 | 10 | if code < 0 { 11 | return fmt.Errorf("error occurred: 0x%X", code) 12 | } 13 | 14 | return nil 15 | } 16 | 17 | func NetworkDeinitialize() error { 18 | code := C.avformat_network_deinit() 19 | 20 | if code < 0 { 21 | return fmt.Errorf("error occurred: 0x%X", code) 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavformat libavcodec libavutil libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import "unsafe" 10 | 11 | // Packet is a piece of encoded data 12 | // acquired from the media container. 13 | // 14 | // It can be either a video frame or 15 | // an audio frame. 16 | type Packet struct { 17 | media *Media 18 | streamIndex int 19 | data []byte 20 | pts int64 21 | dts int64 22 | pos int64 23 | duration int64 24 | size int 25 | flags int 26 | } 27 | 28 | // StreamIndex returns the index of the 29 | // stream the packet belongs to. 30 | func (pkt *Packet) StreamIndex() int { 31 | return pkt.streamIndex 32 | } 33 | 34 | // Type returns the type of the packet 35 | // (video or audio). 36 | func (pkt *Packet) Type() StreamType { 37 | return pkt.media.Streams()[pkt.streamIndex].Type() 38 | } 39 | 40 | // Data returns the data 41 | // encoded in the packet. 42 | func (pkt *Packet) Data() []byte { 43 | buf := make([]byte, pkt.size) 44 | 45 | copy(buf, pkt.data) 46 | 47 | return buf 48 | } 49 | 50 | // Returns the size of the 51 | // packet data. 52 | func (pkt *Packet) Size() int { 53 | return pkt.size 54 | } 55 | 56 | // newPacket creates a 57 | // new packet info object. 58 | func newPacket(media *Media, cPkt *C.AVPacket) *Packet { 59 | pkt := &Packet{ 60 | media: media, 61 | streamIndex: int(cPkt.stream_index), 62 | data: C.GoBytes(unsafe.Pointer( 63 | cPkt.data), cPkt.size), 64 | pts: int64(cPkt.pts), 65 | dts: int64(cPkt.dts), 66 | pos: int64(cPkt.pos), 67 | duration: int64(cPkt.duration), 68 | size: int(cPkt.size), 69 | flags: int(cPkt.flags), 70 | } 71 | 72 | return pkt 73 | } 74 | -------------------------------------------------------------------------------- /pictures/audio_sample_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zergon321/reisen/834b509801375c29aa85da612d8ef29fb8146fbb/pictures/audio_sample_structure.png -------------------------------------------------------------------------------- /platform_darwin.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | import "C" 4 | 5 | func bufferSize(maxBufferSize C.int) C.ulong { 6 | var byteSize C.ulong = 8 7 | return C.ulong(maxBufferSize) * byteSize 8 | } 9 | 10 | func channelLayout(audio *AudioStream) C.longlong { 11 | return C.longlong(audio.codecCtx.channel_layout) 12 | } 13 | 14 | func rewindPosition(dur int64) C.longlong { 15 | return C.longlong(dur) 16 | } 17 | -------------------------------------------------------------------------------- /platform_linux.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | import "C" 4 | 5 | func bufferSize(maxBufferSize C.int) C.ulong { 6 | var byteSize C.ulong = 8 7 | return C.ulong(maxBufferSize) * byteSize 8 | } 9 | 10 | func channelLayout(audio *AudioStream) C.long { 11 | return C.long(audio.codecCtx.channel_layout) 12 | } 13 | 14 | func rewindPosition(dur int64) C.long { 15 | return C.long(dur) 16 | } 17 | -------------------------------------------------------------------------------- /platform_windows.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | import "C" 4 | 5 | func bufferSize(maxBufferSize C.int) C.ulonglong { 6 | var byteSize C.ulonglong = 8 7 | return C.ulonglong(maxBufferSize) * byteSize 8 | } 9 | 10 | func channelLayout(audio *AudioStream) C.longlong { 11 | return C.longlong(audio.codecCtx.channel_layout) 12 | } 13 | 14 | func rewindPosition(dur int64) C.longlong { 15 | return C.longlong(dur) 16 | } 17 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import ( 10 | "fmt" 11 | "time" 12 | "unsafe" 13 | ) 14 | 15 | // StreamType is a type of 16 | // a media stream. 17 | type StreamType int 18 | 19 | const ( 20 | // StreamVideo denotes the stream keeping video frames. 21 | StreamVideo StreamType = C.AVMEDIA_TYPE_VIDEO 22 | // StreamAudio denotes the stream keeping audio frames. 23 | StreamAudio StreamType = C.AVMEDIA_TYPE_AUDIO 24 | ) 25 | 26 | // String returns the string representation of 27 | // stream type identifier. 28 | func (streamType StreamType) String() string { 29 | switch streamType { 30 | case StreamVideo: 31 | return "video" 32 | 33 | case StreamAudio: 34 | return "audio" 35 | 36 | default: 37 | return "" 38 | } 39 | } 40 | 41 | // TODO: add an opportunity to 42 | // receive duration in time base units. 43 | 44 | // Stream is an abstract media data stream. 45 | type Stream interface { 46 | // innerStream returns the inner 47 | // libAV stream of the Stream object. 48 | innerStream() *C.AVStream 49 | 50 | // filter returns the filter context of the stream. 51 | filter() *C.AVBSFContext 52 | // filterIn returns the input 53 | // packet for the stream filter. 54 | filterIn() *C.AVPacket 55 | // filterOut returns the output 56 | // packet for the stream filter. 57 | filterOut() *C.AVPacket 58 | 59 | // open opens the stream for decoding. 60 | open() error 61 | // read decodes the packet and obtains a 62 | // frame from it. 63 | read() (bool, error) 64 | // close closes the stream for decoding. 65 | close() error 66 | 67 | // Index returns the index 68 | // number of the stream. 69 | Index() int 70 | // Type returns the type 71 | // identifier of the stream. 72 | // 73 | // It's either video or audio. 74 | Type() StreamType 75 | // CodecName returns the 76 | // shortened name of the stream codec. 77 | CodecName() string 78 | // CodecLongName returns the 79 | // long name of the stream codec. 80 | CodecLongName() string 81 | // BitRate returns the stream 82 | // bitrate (in bps). 83 | BitRate() int64 84 | // Duration returns the time 85 | // duration of the stream 86 | Duration() (time.Duration, error) 87 | // TimeBase returns the numerator 88 | // and the denominator of the stream 89 | // time base fraction to convert 90 | // time duration in time base units 91 | // of the stream. 92 | TimeBase() (int, int) 93 | // FrameRate returns the approximate 94 | // frame rate (FPS) of the stream. 95 | FrameRate() (int, int) 96 | // FrameCount returns the total number 97 | // of frames in the stream. 98 | FrameCount() int64 99 | // Open opens the stream for decoding. 100 | Open() error 101 | // Rewind rewinds the whole media to the 102 | // specified time location based on the stream. 103 | Rewind(time.Duration) error 104 | // ApplyFilter applies a filter defined 105 | // by the given string to the stream. 106 | ApplyFilter(string) error 107 | // Filter returns the name and arguments 108 | // of the filter currently applied to the 109 | // stream or "" if no filter applied. 110 | Filter() string 111 | // RemoveFilter removes the currently applied 112 | // filter from the stream and frees its memory. 113 | RemoveFilter() error 114 | // ReadFrame decodes the next frame from the stream. 115 | ReadFrame() (Frame, bool, error) 116 | // Closes the stream for decoding. 117 | Close() error 118 | } 119 | 120 | // baseStream holds the information 121 | // common for all media data streams. 122 | type baseStream struct { 123 | media *Media 124 | inner *C.AVStream 125 | codecParams *C.AVCodecParameters 126 | codec *C.AVCodec 127 | codecCtx *C.AVCodecContext 128 | frame *C.AVFrame 129 | filterArgs string 130 | filterCtx *C.AVBSFContext 131 | filterInPacket *C.AVPacket 132 | filterOutPacket *C.AVPacket 133 | skip bool 134 | opened bool 135 | } 136 | 137 | // Opened returns 'true' if the stream 138 | // is opened for decoding, and 'false' otherwise. 139 | func (stream *baseStream) Opened() bool { 140 | return stream.opened 141 | } 142 | 143 | // Index returns the index of the stream. 144 | func (stream *baseStream) Index() int { 145 | return int(stream.inner.index) 146 | } 147 | 148 | // Type returns the stream media data type. 149 | func (stream *baseStream) Type() StreamType { 150 | return StreamType(stream.codecParams.codec_type) 151 | } 152 | 153 | // CodecName returns the name of the codec 154 | // that was used for encoding the stream. 155 | func (stream *baseStream) CodecName() string { 156 | if stream.codec.name == nil { 157 | return "" 158 | } 159 | 160 | return C.GoString(stream.codec.name) 161 | } 162 | 163 | // CodecName returns the long name of the 164 | // codec that was used for encoding the stream. 165 | func (stream *baseStream) CodecLongName() string { 166 | if stream.codec.long_name == nil { 167 | return "" 168 | } 169 | 170 | return C.GoString(stream.codec.long_name) 171 | } 172 | 173 | // BitRate returns the bit rate of the stream (in bps). 174 | func (stream *baseStream) BitRate() int64 { 175 | return int64(stream.codecParams.bit_rate) 176 | } 177 | 178 | // Duration returns the duration of the stream. 179 | func (stream *baseStream) Duration() (time.Duration, error) { 180 | dur := stream.inner.duration 181 | 182 | if dur < 0 { 183 | dur = 0 184 | } 185 | 186 | tmNum, tmDen := stream.TimeBase() 187 | factor := float64(tmNum) / float64(tmDen) 188 | tm := float64(dur) * factor 189 | 190 | return time.ParseDuration(fmt.Sprintf("%fs", tm)) 191 | } 192 | 193 | // TimeBase the numerator and the denominator of the 194 | // stream time base factor fraction. 195 | // 196 | // All the duration values of the stream are 197 | // multiplied by this factor to get duration 198 | // in seconds. 199 | func (stream *baseStream) TimeBase() (int, int) { 200 | return int(stream.inner.time_base.num), 201 | int(stream.inner.time_base.den) 202 | } 203 | 204 | // FrameRate returns the frame rate of the stream 205 | // as a fraction with a numerator and a denominator. 206 | func (stream *baseStream) FrameRate() (int, int) { 207 | return int(stream.inner.r_frame_rate.num), 208 | int(stream.inner.r_frame_rate.den) 209 | } 210 | 211 | // FrameCount returns the total number of frames 212 | // in the stream. 213 | func (stream *baseStream) FrameCount() int64 { 214 | return int64(stream.inner.nb_frames) 215 | } 216 | 217 | // ApplyFilter applies a filter defined 218 | // by the given string to the stream. 219 | func (stream *baseStream) ApplyFilter(args string) error { 220 | status := C.av_bsf_list_parse_str( 221 | C.CString(args), &stream.filterCtx) 222 | 223 | if status < 0 { 224 | return fmt.Errorf( 225 | "%d: couldn't create a filter context", status) 226 | } 227 | 228 | status = C.avcodec_parameters_copy(stream.filterCtx.par_in, stream.codecParams) 229 | 230 | if status < 0 { 231 | return fmt.Errorf( 232 | "%d: couldn't copy the input codec parameters to the filter", status) 233 | } 234 | 235 | status = C.avcodec_parameters_copy(stream.filterCtx.par_out, stream.codecParams) 236 | 237 | if status < 0 { 238 | return fmt.Errorf( 239 | "%d: couldn't copy the output codec parameters to the filter", status) 240 | } 241 | 242 | stream.filterCtx.time_base_in = stream.inner.time_base 243 | stream.filterCtx.time_base_out = stream.inner.time_base 244 | 245 | status = C.av_bsf_init(stream.filterCtx) 246 | 247 | if status < 0 { 248 | return fmt.Errorf( 249 | "%d: couldn't initialize the filter context", status) 250 | } 251 | 252 | stream.filterInPacket = C.av_packet_alloc() 253 | 254 | if stream.filterInPacket == nil { 255 | return fmt.Errorf( 256 | "couldn't allocate a packet for filtering in") 257 | } 258 | 259 | stream.filterOutPacket = C.av_packet_alloc() 260 | 261 | if stream.filterInPacket == nil { 262 | return fmt.Errorf( 263 | "couldn't allocate a packet for filtering out") 264 | } 265 | 266 | stream.filterArgs = args 267 | 268 | return nil 269 | } 270 | 271 | // Filter returns the name and arguments 272 | // of the filter currently applied to the 273 | // stream or "" if no filter applied. 274 | func (stream *baseStream) Filter() string { 275 | return stream.filterArgs 276 | } 277 | 278 | // RemoveFilter removes the currently applied 279 | // filter from the stream and frees its memory. 280 | func (stream *baseStream) RemoveFilter() error { 281 | if stream.filterCtx == nil { 282 | return fmt.Errorf("no filter applied") 283 | } 284 | 285 | C.av_bsf_free(&stream.filterCtx) 286 | stream.filterCtx = nil 287 | 288 | C.av_free(unsafe.Pointer(stream.filterInPacket)) 289 | stream.filterInPacket = nil 290 | C.av_free(unsafe.Pointer(stream.filterOutPacket)) 291 | stream.filterOutPacket = nil 292 | 293 | return nil 294 | } 295 | 296 | // Rewind rewinds the stream to 297 | // the specified time position. 298 | // 299 | // Can be used on all the types 300 | // of streams. However, it's better 301 | // to use it on the video stream of 302 | // the media file if you don't want 303 | // the streams of the playback to 304 | // desynchronyze. 305 | func (stream *baseStream) Rewind(t time.Duration) error { 306 | tmNum, tmDen := stream.TimeBase() 307 | factor := float64(tmDen) / float64(tmNum) 308 | seconds := t.Seconds() 309 | dur := int64(seconds * factor) 310 | 311 | status := C.av_seek_frame(stream.media.ctx, 312 | stream.inner.index, rewindPosition(dur), 313 | C.AVSEEK_FLAG_FRAME|C.AVSEEK_FLAG_BACKWARD) 314 | 315 | if status < 0 { 316 | return fmt.Errorf( 317 | "%d: couldn't rewind the stream", status) 318 | } 319 | 320 | return nil 321 | } 322 | 323 | // innerStream returns the inner 324 | // libAV stream of the Stream object. 325 | func (stream *baseStream) innerStream() *C.AVStream { 326 | return stream.inner 327 | } 328 | 329 | // filter returns the filter context of the stream. 330 | func (stream *baseStream) filter() *C.AVBSFContext { 331 | return stream.filterCtx 332 | } 333 | 334 | // filterIn returns the input 335 | // packet for the stream filter. 336 | func (stream *baseStream) filterIn() *C.AVPacket { 337 | return stream.filterInPacket 338 | } 339 | 340 | // filterOut returns the output 341 | // packet for the stream filter. 342 | func (stream *baseStream) filterOut() *C.AVPacket { 343 | return stream.filterOutPacket 344 | } 345 | 346 | // open opens the stream for decoding. 347 | func (stream *baseStream) open() error { 348 | stream.codecCtx = C.avcodec_alloc_context3(stream.codec) 349 | 350 | if stream.codecCtx == nil { 351 | return fmt.Errorf("couldn't open a codec context") 352 | } 353 | 354 | status := C.avcodec_parameters_to_context( 355 | stream.codecCtx, stream.codecParams) 356 | 357 | if status < 0 { 358 | return fmt.Errorf( 359 | "%d: couldn't send codec parameters to the context", status) 360 | } 361 | 362 | status = C.avcodec_open2(stream.codecCtx, stream.codec, nil) 363 | 364 | if status < 0 { 365 | return fmt.Errorf( 366 | "%d: couldn't open the codec context", status) 367 | } 368 | 369 | stream.frame = C.av_frame_alloc() 370 | 371 | if stream.frame == nil { 372 | return fmt.Errorf( 373 | "couldn't allocate a new frame") 374 | } 375 | 376 | stream.opened = true 377 | 378 | return nil 379 | } 380 | 381 | // read decodes the packet and obtains a 382 | // frame from it. 383 | func (stream *baseStream) read() (bool, error) { 384 | readPacket := stream.media.packet 385 | 386 | if stream.filterCtx != nil { 387 | readPacket = stream.filterOutPacket 388 | } 389 | 390 | status := C.avcodec_send_packet( 391 | stream.codecCtx, readPacket) 392 | 393 | if status < 0 { 394 | stream.skip = false 395 | 396 | return false, fmt.Errorf( 397 | "%d: couldn't send the packet to the codec context", status) 398 | } 399 | 400 | status = C.avcodec_receive_frame( 401 | stream.codecCtx, stream.frame) 402 | 403 | if status < 0 { 404 | if status == C.int(ErrorAgain) { 405 | stream.skip = true 406 | return true, nil 407 | } 408 | 409 | stream.skip = false 410 | 411 | return false, fmt.Errorf( 412 | "%d: couldn't receive the frame from the codec context", status) 413 | } 414 | 415 | C.av_packet_unref(stream.media.packet) 416 | 417 | if stream.filterInPacket != nil { 418 | C.av_packet_unref(stream.filterInPacket) 419 | } 420 | 421 | if stream.filterOutPacket != nil { 422 | C.av_packet_unref(stream.filterOutPacket) 423 | } 424 | 425 | stream.skip = false 426 | 427 | return true, nil 428 | } 429 | 430 | // close closes the stream for decoding. 431 | func (stream *baseStream) close() error { 432 | C.av_free(unsafe.Pointer(stream.frame)) 433 | stream.frame = nil 434 | 435 | status := C.avcodec_close(stream.codecCtx) 436 | 437 | if status < 0 { 438 | return fmt.Errorf( 439 | "%d: couldn't close the codec", status) 440 | } 441 | 442 | if stream.filterCtx != nil { 443 | C.av_bsf_free(&stream.filterCtx) 444 | stream.filterCtx = nil 445 | } 446 | 447 | if stream.filterInPacket != nil { 448 | C.av_free(unsafe.Pointer(stream.filterInPacket)) 449 | stream.filterInPacket = nil 450 | } 451 | 452 | if stream.filterOutPacket != nil { 453 | C.av_free(unsafe.Pointer(stream.filterOutPacket)) 454 | stream.filterOutPacket = nil 455 | } 456 | 457 | stream.opened = false 458 | 459 | return nil 460 | } 461 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil 4 | // #include 5 | import "C" 6 | 7 | const ( 8 | // TimeBase is a global time base 9 | // used for describing media containers. 10 | TimeBase int = C.AV_TIME_BASE 11 | ) 12 | -------------------------------------------------------------------------------- /unknown.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | // #include 10 | import "C" 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | // UnknownStream is a stream containing frames consisting of unknown data. 16 | type UnknownStream struct { 17 | baseStream 18 | } 19 | 20 | // Open is just a stub. 21 | func (unknown *UnknownStream) Open() error { 22 | return nil 23 | } 24 | 25 | // ReadFrame is just a stub. 26 | func (unknown *UnknownStream) ReadFrame() (Frame, bool, error) { 27 | return nil, false, fmt.Errorf("UnknownStream.ReadFrame() not implemented") 28 | } 29 | 30 | // Close is just a stub. 31 | func (unknown *UnknownStream) Close() error { 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /video.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | // #include 10 | import "C" 11 | import ( 12 | "fmt" 13 | "unsafe" 14 | ) 15 | 16 | // VideoStream is a streaming holding 17 | // video frames. 18 | type VideoStream struct { 19 | baseStream 20 | swsCtx *C.struct_SwsContext 21 | rgbaFrame *C.AVFrame 22 | bufSize C.int 23 | } 24 | 25 | // AspectRatio returns the fraction of the video 26 | // stream frame aspect ratio (1/0 if unknown). 27 | func (video *VideoStream) AspectRatio() (int, int) { 28 | return int(video.codecParams.sample_aspect_ratio.num), 29 | int(video.codecParams.sample_aspect_ratio.den) 30 | } 31 | 32 | // Width returns the width of the video 33 | // stream frame. 34 | func (video *VideoStream) Width() int { 35 | return int(video.codecParams.width) 36 | } 37 | 38 | // Height returns the height of the video 39 | // stream frame. 40 | func (video *VideoStream) Height() int { 41 | return int(video.codecParams.height) 42 | } 43 | 44 | // OpenDecode opens the video stream for 45 | // decoding with default parameters. 46 | func (video *VideoStream) Open() error { 47 | return video.OpenDecode( 48 | int(video.codecParams.width), 49 | int(video.codecParams.height), 50 | InterpolationBicubic) 51 | } 52 | 53 | // OpenDecode opens the video stream for 54 | // decoding with the specified parameters. 55 | func (video *VideoStream) OpenDecode(width, height int, alg InterpolationAlgorithm) error { 56 | err := video.open() 57 | 58 | if err != nil { 59 | return err 60 | } 61 | 62 | video.rgbaFrame = C.av_frame_alloc() 63 | 64 | if video.rgbaFrame == nil { 65 | return fmt.Errorf( 66 | "couldn't allocate a new RGBA frame") 67 | } 68 | 69 | video.bufSize = C.av_image_get_buffer_size( 70 | C.AV_PIX_FMT_RGBA, C.int(width), C.int(height), 1) 71 | 72 | if video.bufSize < 0 { 73 | return fmt.Errorf( 74 | "%d: couldn't get the buffer size", video.bufSize) 75 | } 76 | 77 | buf := (*C.uint8_t)(unsafe.Pointer( 78 | C.av_malloc(bufferSize(video.bufSize)))) 79 | 80 | if buf == nil { 81 | return fmt.Errorf( 82 | "couldn't allocate an AV buffer") 83 | } 84 | 85 | status := C.av_image_fill_arrays(&video.rgbaFrame.data[0], 86 | &video.rgbaFrame.linesize[0], buf, C.AV_PIX_FMT_RGBA, 87 | C.int(width), C.int(height), 1) 88 | 89 | if status < 0 { 90 | return fmt.Errorf( 91 | "%d: couldn't fill the image arrays", status) 92 | } 93 | 94 | video.swsCtx = C.sws_getContext(video.codecCtx.width, 95 | video.codecCtx.height, video.codecCtx.pix_fmt, 96 | C.int(width), C.int(height), 97 | C.AV_PIX_FMT_RGBA, C.int(alg), nil, nil, nil) 98 | 99 | if video.swsCtx == nil { 100 | return fmt.Errorf( 101 | "couldn't create an SWS context") 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // ReadFrame reads the next frame from the stream. 108 | func (video *VideoStream) ReadFrame() (Frame, bool, error) { 109 | return video.ReadVideoFrame() 110 | } 111 | 112 | // ReadVideoFrame reads the next video frame 113 | // from the video stream. 114 | func (video *VideoStream) ReadVideoFrame() (*VideoFrame, bool, error) { 115 | ok, err := video.read() 116 | 117 | if err != nil { 118 | return nil, false, err 119 | } 120 | 121 | if ok && video.skip { 122 | return nil, true, nil 123 | } 124 | 125 | // No more data. 126 | if !ok { 127 | return nil, false, nil 128 | } 129 | 130 | C.sws_scale(video.swsCtx, &video.frame.data[0], 131 | &video.frame.linesize[0], 0, 132 | video.codecCtx.height, 133 | &video.rgbaFrame.data[0], 134 | &video.rgbaFrame.linesize[0]) 135 | 136 | data := C.GoBytes(unsafe. 137 | Pointer(video.rgbaFrame.data[0]), 138 | video.bufSize) 139 | frame := newVideoFrame(video, int64(video.frame.pts), 140 | int(video.frame.coded_picture_number), 141 | int(video.frame.display_picture_number), 142 | int(video.codecCtx.width), int(video.codecCtx.height), data) 143 | 144 | return frame, true, nil 145 | } 146 | 147 | // Close closes the video stream for decoding. 148 | func (video *VideoStream) Close() error { 149 | err := video.close() 150 | 151 | if err != nil { 152 | return err 153 | } 154 | 155 | C.av_free(unsafe.Pointer(video.rgbaFrame)) 156 | video.rgbaFrame = nil 157 | C.sws_freeContext(video.swsCtx) 158 | video.swsCtx = nil 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /videoframe.go: -------------------------------------------------------------------------------- 1 | package reisen 2 | 3 | // #cgo pkg-config: libavutil libavformat libavcodec libswscale 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | // #include 10 | import "C" 11 | import "image" 12 | 13 | // VideoFrame is a single frame 14 | // of a video stream. 15 | type VideoFrame struct { 16 | baseFrame 17 | img *image.RGBA 18 | } 19 | 20 | // Data returns a byte slice of RGBA 21 | // pixels of the frame image. 22 | func (frame *VideoFrame) Data() []byte { 23 | return frame.img.Pix 24 | } 25 | 26 | // Image returns the RGBA image of the frame. 27 | func (frame *VideoFrame) Image() *image.RGBA { 28 | return frame.img 29 | } 30 | 31 | // newVideoFrame returns a newly created video frame. 32 | func newVideoFrame(stream Stream, pts int64, indCoded, indDisplay, width, height int, pix []byte) *VideoFrame { 33 | upLeft := image.Point{0, 0} 34 | lowRight := image.Point{width, height} 35 | img := image.NewRGBA(image.Rectangle{upLeft, lowRight}) 36 | frame := new(VideoFrame) 37 | 38 | img.Pix = pix 39 | frame.stream = stream 40 | frame.pts = pts 41 | frame.img = img 42 | frame.indexCoded = indCoded 43 | frame.indexDisplay = indDisplay 44 | 45 | return frame 46 | } 47 | --------------------------------------------------------------------------------