Skip to content

Commit

Permalink
add more examples (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 authored Jan 1, 2024
1 parent 43baf00 commit b663460
Show file tree
Hide file tree
Showing 17 changed files with 655 additions and 72 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
with:
go-version: ${{ matrix.go }}

- run: sudo apt update && sudo apt install -y libavformat-dev libswscale-dev

- run: make test-nodocker

- if: matrix.go == '1.21'
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Features:
* [playlist-parser](examples/playlist-parser/main.go)
* [client](examples/client/main.go)
* [client-ntp-timestamp](examples/client-ntp-timestamp/main.go)
* [client-codec-h264-save-to-disk](examples/client-codec-h264-save-to-disk/main.go)
* [client-codec-h264-convert-to-jpeg](examples/client-codec-h264-convert-to-jpeg/main.go)
* [client-codec-mpeg4audio-save-to-disk](examples/client-codec-mpeg4audio-save-to-disk/main.go)
* [muxer](examples/muxer/main.go)

## API Documentation
Expand Down
137 changes: 137 additions & 0 deletions examples/client-codec-h264-convert-to-jpeg/h264_decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"fmt"
"image"
"unsafe"
)

// #cgo pkg-config: libavcodec libavutil libswscale
// #include <libavcodec/avcodec.h>
// #include <libavutil/imgutils.h>
// #include <libswscale/swscale.h>
import "C"

func frameData(frame *C.AVFrame) **C.uint8_t {
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
}

func frameLineSize(frame *C.AVFrame) *C.int {
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
}

// h264Decoder is a wrapper around FFmpeg's H264 decoder.
type h264Decoder struct {
codecCtx *C.AVCodecContext
srcFrame *C.AVFrame
swsCtx *C.struct_SwsContext
dstFrame *C.AVFrame
dstFramePtr []uint8
}

// initialize initializes a h264Decoder.
func (d *h264Decoder) initialize() error {
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
if codec == nil {
return fmt.Errorf("avcodec_find_decoder() failed")
}

d.codecCtx = C.avcodec_alloc_context3(codec)
if d.codecCtx == nil {
return fmt.Errorf("avcodec_alloc_context3() failed")
}

res := C.avcodec_open2(d.codecCtx, codec, nil)
if res < 0 {
C.avcodec_close(d.codecCtx)
return fmt.Errorf("avcodec_open2() failed")
}

d.srcFrame = C.av_frame_alloc()
if d.srcFrame == nil {
C.avcodec_close(d.codecCtx)
return fmt.Errorf("av_frame_alloc() failed")
}

return nil
}

// close closes the decoder.
func (d *h264Decoder) close() {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}

if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}

C.av_frame_free(&d.srcFrame)
C.avcodec_close(d.codecCtx)
}

func (d *h264Decoder) decode(nalu []byte) (image.Image, error) {
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)

// send NALU to decoder
var avPacket C.AVPacket
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
defer C.free(unsafe.Pointer(avPacket.data))
avPacket.size = C.int(len(nalu))
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
if res < 0 {
return nil, nil
}

// receive frame if available
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
if res < 0 {
return nil, nil
}

// if frame size has changed, allocate needed objects
if d.dstFrame == nil || d.dstFrame.width != d.srcFrame.width || d.dstFrame.height != d.srcFrame.height {
if d.dstFrame != nil {
C.av_frame_free(&d.dstFrame)
}

if d.swsCtx != nil {
C.sws_freeContext(d.swsCtx)
}

d.dstFrame = C.av_frame_alloc()
d.dstFrame.format = C.AV_PIX_FMT_RGBA
d.dstFrame.width = d.srcFrame.width
d.dstFrame.height = d.srcFrame.height
d.dstFrame.color_range = C.AVCOL_RANGE_JPEG
res = C.av_frame_get_buffer(d.dstFrame, 1)
if res < 0 {
return nil, fmt.Errorf("av_frame_get_buffer() failed")
}

d.swsCtx = C.sws_getContext(d.srcFrame.width, d.srcFrame.height, C.AV_PIX_FMT_YUV420P,
d.dstFrame.width, d.dstFrame.height, (int32)(d.dstFrame.format), C.SWS_BILINEAR, nil, nil, nil)
if d.swsCtx == nil {
return nil, fmt.Errorf("sws_getContext() failed")
}

dstFrameSize := C.av_image_get_buffer_size((int32)(d.dstFrame.format), d.dstFrame.width, d.dstFrame.height, 1)
d.dstFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.dstFrame.data[0]))[:dstFrameSize:dstFrameSize]
}

// convert color space from YUV420 to RGBA
res = C.sws_scale(d.swsCtx, frameData(d.srcFrame), frameLineSize(d.srcFrame),
0, d.srcFrame.height, frameData(d.dstFrame), frameLineSize(d.dstFrame))
if res < 0 {
return nil, fmt.Errorf("sws_scale() failed")
}

// embed frame into an image.Image
return &image.RGBA{
Pix: d.dstFramePtr,
Stride: 4 * (int)(d.dstFrame.width),
Rect: image.Rectangle{
Max: image.Point{(int)(d.dstFrame.width), (int)(d.dstFrame.height)},
},
}, nil
}
124 changes: 124 additions & 0 deletions examples/client-codec-h264-convert-to-jpeg/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"fmt"
"image"
"image/jpeg"
"log"
"os"
"strconv"
"time"

"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
)

// This example shows how to
// 1. read a HLS stream
// 2. check if there's a H264 track
// 3. decode H264 access units into RGBA frames
// 4. convert frames to JPEG images and save them on disk

// This example requires the FFmpeg libraries, that can be installed with this command:
// apt install -y libavformat-dev libswscale-dev gcc pkg-config

func findH264Track(tracks []*gohlslib.Track) *gohlslib.Track {
for _, track := range tracks {
if _, ok := track.Codec.(*codecs.H264); ok {
return track
}
}
return nil
}

func saveToFile(img image.Image) error {
// create file
fname := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + ".jpg"
f, err := os.Create(fname)
if err != nil {
panic(err)
}
defer f.Close()

log.Println("saving", fname)

// convert to jpeg
return jpeg.Encode(f, img, &jpeg.Options{
Quality: 60,
})
}

func main() {
// setup client
c := &gohlslib.Client{
URI: "https://myserver/mystream/index.m3u8",
}

// called when tracks are parsed
c.OnTracks = func(tracks []*gohlslib.Track) error {
// find the H264 track
track := findH264Track(tracks)
if track == nil {
return fmt.Errorf("H264 track not found")
}

// create the H264 decoder
frameDec := &h264Decoder{}
err := frameDec.initialize()
if err != nil {
return err
}

// if SPS and PPS are present into the track, send them to the decoder
if track.Codec.(*codecs.H264).SPS != nil {
frameDec.decode(track.Codec.(*codecs.H264).SPS)
}
if track.Codec.(*codecs.H264).PPS != nil {
frameDec.decode(track.Codec.(*codecs.H264).PPS)
}

saveCount := 0

// set a callback that is called when data is received
c.OnDataH26x(track, func(pts time.Duration, dts time.Duration, au [][]byte) {
log.Printf("received access unit with pts = %v\n", pts)

for _, nalu := range au {
// convert NALUs into RGBA frames
img, err := frameDec.decode(nalu)
if err != nil {
panic(err)
}

// wait for a frame
if img == nil {
continue
}

// convert frame to JPEG and save to file
err = saveToFile(img)
if err != nil {
panic(err)
}

saveCount++
if saveCount == 5 {
log.Printf("saved 5 images, exiting")
os.Exit(1)
}
}
})

return nil
}

// start reading
err := c.Start()
if err != nil {
panic(err)
}
defer c.Close()

// wait for a fatal error
panic(<-c.Wait())
}
74 changes: 74 additions & 0 deletions examples/client-codec-h264-save-to-disk/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"fmt"
"log"
"time"

"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
)

// This example shows how to
// 1. read a HLS stream
// 2. check if there's a H264 track
// 3. save the H264 track to disk in MPEG-TS format

func findH264Track(tracks []*gohlslib.Track) *gohlslib.Track {
for _, track := range tracks {
if _, ok := track.Codec.(*codecs.H264); ok {
return track
}
}
return nil
}

func main() {
// setup client
c := &gohlslib.Client{
URI: "https://myserver/mystream/index.m3u8",
}

// called when tracks are parsed
c.OnTracks = func(tracks []*gohlslib.Track) error {
// find the H264 track
track := findH264Track(tracks)
if track == nil {
return fmt.Errorf("H264 track not found")
}

// create the MPEG-TS muxer
m := &mpegtsMuxer{
fileName: "mystream.ts",
sps: track.Codec.(*codecs.H264).SPS,
pps: track.Codec.(*codecs.H264).PPS,
}
err := m.initialize()
if err != nil {
return nil
}

// set a callback that is called when data is received
c.OnDataH26x(track, func(pts time.Duration, dts time.Duration, au [][]byte) {
log.Printf("received access unit with pts = %v\n", pts)

// send data to the MPEG-TS muxer
err := m.writeH264(au, pts)
if err != nil {
panic(err)
}
})

return nil
}

// start reading
err := c.Start()
if err != nil {
panic(err)
}
defer c.Close()

// wait for a fatal error
panic(<-c.Wait())
}
Loading

0 comments on commit b663460

Please sign in to comment.