Skip to content

Instantly share code, notes, and snippets.

@s-macke
Last active January 8, 2020 14:42
Show Gist options
  • Save s-macke/f0a965cd38e51734a53520761a08b045 to your computer and use it in GitHub Desktop.
Save s-macke/f0a965cd38e51734a53520761a08b045 to your computer and use it in GitHub Desktop.
ASCIICinema: Show movie files on the console via "curl --compressed localhost:12345"
package main
import "C"
import (
"fmt"
"github.com/giorgisio/goav/avcodec"
"github.com/giorgisio/goav/avutil"
"github.com/giorgisio/goav/swscale"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/NYTimes/gziphandler"
"image"
"image/png"
"log"
"net"
"net/http"
"os"
"sync"
"unsafe"
)
import "github.com/giorgisio/goav/avformat"
type ffmpegvideo struct {
filename string
videostreamid int
pFormatContext *avformat.Context
pCodecCtx *avcodec.Context
pCodecCtxOrig *avformat.CodecContext
pCodec *avcodec.Codec
targetwidth, targetheight int
pFrame *avutil.Frame
pFrameRGB *avutil.Frame
buffer unsafe.Pointer
avp *avcodec.Picture
swsCtx *swscale.Context
packet *avcodec.Packet
}
func (video *ffmpegvideo) Init(filename string, targetwidth, targetheight int) {
video.filename = filename
video.targetwidth = targetwidth
video.targetheight = targetheight
// Open video file
video.pFormatContext = avformat.AvformatAllocContext()
if avformat.AvformatOpenInput(&video.pFormatContext, video.filename, nil, nil) != 0 {
fmt.Printf("Unable to open file %s\n", video.filename)
os.Exit(1)
}
// Retrieve stream information
if video.pFormatContext.AvformatFindStreamInfo(nil) < 0 {
fmt.Println("Couldn't find stream information")
os.Exit(1)
}
// Dump information about file onto standard error
fmt.Println("--------------------------------------------------------------")
video.pFormatContext.AvDumpFormat(0, os.Args[1], 0)
fmt.Println("--------------------------------------------------------------")
video.GetCodecCtx()
// Allocate video frame
video.pFrame = avutil.AvFrameAlloc()
// Allocate an AVFrame structure
video.pFrameRGB = avutil.AvFrameAlloc()
if video.pFrameRGB == nil {
fmt.Println("Unable to allocate RGB Frame")
os.Exit(1)
}
// Determine required buffer size and allocate buffer
numBytes := uintptr(avcodec.AvpictureGetSize(avcodec.AV_PIX_FMT_RGBA, video.targetwidth, video.targetheight))
video.buffer = avutil.AvMalloc(numBytes)
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
video.avp = (*avcodec.Picture)(unsafe.Pointer(video.pFrameRGB))
video.avp.AvpictureFill((*uint8)(video.buffer), avcodec.AV_PIX_FMT_RGBA, video.targetwidth, video.targetheight)
// initialize SWS context for software scaling
video.swsCtx = swscale.SwsGetcontext(
video.pCodecCtx.Width(),
video.pCodecCtx.Height(),
(swscale.PixelFormat)(video.pCodecCtx.PixFmt()),
video.targetwidth,
video.targetheight,
avcodec.AV_PIX_FMT_RGBA,
avcodec.SWS_BILINEAR,
nil,
nil,
nil,
)
video.packet = avcodec.AvPacketAlloc()
}
func (video *ffmpegvideo) Free() {
// Free the RGB image
avutil.AvFree(video.buffer)
avutil.AvFrameFree(video.pFrameRGB)
// Free the YUV frame
avutil.AvFrameFree(video.pFrame)
// Close the codecs
video.pCodecCtx.AvcodecClose()
(*avcodec.Context)(unsafe.Pointer(video.pCodecCtxOrig)).AvcodecClose()
// Close the video file
video.pFormatContext.AvformatCloseInput()
// Stop after saving frames of first video straem
}
func (video *ffmpegvideo) GetCodecCtx() {
video.videostreamid = -1
// Find the first video stream
for i := 0; i < int(video.pFormatContext.NbStreams()); i++ {
if video.pFormatContext.Streams()[i].CodecParameters().AvCodecGetType() == avformat.AVMEDIA_TYPE_VIDEO {
video.videostreamid = i;
}
}
if video.videostreamid == -1 {
fmt.Println("Didn't find a video stream")
os.Exit(1)
}
// Get a pointer to the codec context for the video stream
video.pCodecCtxOrig = video.pFormatContext.Streams()[video.videostreamid].Codec()
// Find the decoder for the video stream
video.pCodec = avcodec.AvcodecFindDecoder(avcodec.CodecId(video.pCodecCtxOrig.GetCodecId()))
if video.pCodec == nil {
fmt.Println("Unsupported codec!")
os.Exit(1)
}
// Copy context
video.pCodecCtx = video.pCodec.AvcodecAllocContext3()
if video.pCodecCtx.AvcodecCopyContext((*avcodec.Context)(unsafe.Pointer(video.pCodecCtxOrig))) != 0 {
fmt.Println("Couldn't copy codec context")
os.Exit(1)
}
// Open codec
if video.pCodecCtx.AvcodecOpen2(video.pCodec, nil) < 0 {
fmt.Println("Could not open codec")
os.Exit(1)
}
}
func (video *ffmpegvideo) ReceiveNextFrame() int {
response := video.pCodecCtx.AvcodecReceiveFrame((*avcodec.Frame)(unsafe.Pointer(video.pFrame)))
//fmt.Println("V1:", response)
if response == 0 {
return 0
}
video.packet.AvFreePacket()
//if response == avutil.AvErrorEAGAIN || response == avutil.AvErrorEOF {
if response == avutil.AvErrorEOF {
fmt.Printf("Stream end: %s\n", avutil.ErrorFromCode(response))
os.Exit(1)
} else if response == -11 { // EAGAIN
} else if response < 0 {
fmt.Println(response)
fmt.Printf("Error while receiving a frame from the decoder: %s\n", avutil.ErrorFromCode(response))
return response
}
for {
response = video.pFormatContext.AvReadFrame(video.packet);
if response < 0 {
fmt.Println(response)
fmt.Printf("Error while receiving a frame from the decoder: %s\n", avutil.ErrorFromCode(response))
return response
}
// Is this a packet from the video stream?
if video.packet.StreamIndex() == video.videostreamid {
// Decode video frame
response := video.pCodecCtx.AvcodecSendPacket(video.packet)
if response < 0 {
fmt.Printf("Error while sending a packet to the decoder: %s\n", avutil.ErrorFromCode(response))
}
for response >= 0 {
response = video.pCodecCtx.AvcodecReceiveFrame((*avcodec.Frame)(unsafe.Pointer(video.pFrame)))
//if response == avutil.AvErrorEAGAIN || response == avutil.AvErrorEOF {
if response == -11 || response == avutil.AvErrorEOF {
break
} else if response < 0 {
fmt.Println(response)
fmt.Printf("Error while receiving a frame from the decoder: %s\n", avutil.ErrorFromCode(response))
return response;
}
return 0;
}
}
// Free the packet that was allocated by av_read_frame
video.packet.AvFreePacket()
}
}
func (video *ffmpegvideo) Scale() {
swscale.SwsScale2(
video.swsCtx,
avutil.Data(video.pFrame),
avutil.Linesize(video.pFrame),
0,
video.pCodecCtx.Height(),
avutil.Data(video.pFrameRGB),
avutil.Linesize(video.pFrameRGB))
}
var lastTimestamp int64 = 0
func (video *ffmpegvideo) Wait() {
timebase := video.pCodecCtx.AvCodecGetPktTimebase()
rat := float32(timebase.Num()) / float32(timebase.Den())*1000.*1000.
currentTimestamp := avutil.GetBestEffortTimestamp(video.pFrame)
//is->video_st->time_base
if lastTimestamp > currentTimestamp {
lastTimestamp = currentTimestamp;
return;
}
if (currentTimestamp - lastTimestamp) > 200000 {
lastTimestamp = currentTimestamp;
return;
}
//fmt.Println((float32(currentTimestamp - lastTimestamp)) * rat)
time.Sleep(time.Duration((float32(currentTimestamp - lastTimestamp)) * rat) * time.Microsecond)
lastTimestamp = currentTimestamp;
}
func StoreImage(img image.Image) {
// outputFile is a File type which satisfies Writer interface
outputFile, err := os.Create("test.png")
if err != nil {
log.Fatal(err)
}
png.Encode(outputFile, img)
outputFile.Close()
}
// -------------------------------------
var sharedText string
var framenumber int32
var condition *sync.Cond
var nconnections int64 = 0
// -------------------------------------
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("\033[H\033[2J"))
n := framenumber
atomic.AddInt64(&nconnections, 1)
defer atomic.AddInt64(&nconnections, -1)
for {
condition.L.Lock()
for framenumber == n {
condition.Wait()
}
n = framenumber
condition.L.Unlock()
_, err := w.Write([]byte(sharedText))
if (err != nil) {
fmt.Println(err)
return
}
}
}
// -------------------------------------
func ToText(img image.Image) string {
var sb strings.Builder
for j:=0; j<img.Bounds().Size().Y>>1; j++ {
for i := 0; i < img.Bounds().Size().X>>1; i++ {
rb, gb, bb, _ := img.At(i<<1, j<<1).RGBA()
r := int((rb >> 8) & 0xFF);
g := int((gb >> 8) & 0xFF);
b := int((bb >> 8) & 0xFF);
sb.WriteString("\033[38;2;" + strconv.Itoa(r) +";"+ strconv.Itoa(g) + ";" + strconv.Itoa(b) + "m")
rb, gb, bb, _ = img.At(i<<1, (j<<1)+1).RGBA()
r = int((rb >> 8) & 0xFF);
g = int((gb >> 8) & 0xFF);
b = int((bb >> 8) & 0xFF);
sb.WriteString("\033[48;2;" + strconv.Itoa(r) +";"+ strconv.Itoa(g) + ";" + strconv.Itoa(b) + "m")
sb.WriteRune('▀')
}
sb.WriteString("\n")
}
return sb.String()
}
func DecodeVideo() {
var video ffmpegvideo
video.Init(os.Args[1], 50*4, 40*2)
img := image.NewRGBA(image.Rect(0,0, video.targetwidth, video.targetheight))
for {
//video.ReceiveNextFrame()
response := video.ReceiveNextFrame()
if response == avutil.AvErrorEOF {
video.pFormatContext.AvformatSeekFile(video.videostreamid, 0, 0, 0, 0)
continue;
}
video.Scale()
video.Wait()
if nconnections <= 0 {
time.Sleep(1 * time.Second)
}
//fmt.Println(nconnections)
//fmt.Println("received frame")
var data0 *uint8
data0 = avutil.Data(video.pFrameRGB)[0]
data := uintptr(unsafe.Pointer(data0))
for i := 0; i<video.targetwidth*video.targetheight*4; i++ {
img.Pix[i] = *(*uint8)(unsafe.Pointer(data+uintptr(i)))
}
str := ToText(img);
condition.L.Lock()
//sharedText = "\033[;H" + handle.Text()
sharedText = "\033[;H" + "\033[0m" + "Serving " + strconv.Itoa(int(nconnections)) + " connections. https://github.com/s-macke\n" + str
framenumber++
condition.Broadcast()
condition.L.Unlock()
}
video.Free()
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Please provide a movie file")
os.Exit(1)
}
m := sync.Mutex{}
condition = sync.NewCond(&m)
withoutGz := http.HandlerFunc(handler)
withGz := gziphandler.GzipHandler(withoutGz)
go func() {
log.Fatal(http.ListenAndServe(":12345", withGz))
}();
for {
DecodeVideo()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment