Last active
January 8, 2020 14:42
-
-
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"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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