// Package text transcode a video to text package text import ( "bufio" "bytes" "fmt" "io" "log" "os/exec" "gitlab.crans.org/nounous/ghostream/messaging" ) // Options holds text package configuration type Options struct { Enabled bool Width int Height int Framerate int } // Init text transcoder func Init(streams *messaging.Streams, cfg *Options) { if !cfg.Enabled { // Text transcode is not enabled, ignore return } // Subscribe to new stream event event := make(chan string, 8) streams.Subscribe(event) // For each new stream for name := range event { // Get stream stream, err := streams.Get(name) if err != nil { log.Printf("Failed to get stream '%s'", name) } // Get specific quality // FIXME: make it possible to forward other qualities qualityName := "source" quality, err := stream.GetQuality(qualityName) if err != nil { log.Printf("Failed to get quality '%s'", qualityName) } // Create new text quality outputQuality, err := stream.CreateQuality("text") if err != nil { log.Printf("Failed to create quality 'text': %s", err) } // Start forwarding log.Printf("Starting text transcoder for '%s' quality '%s'", name, qualityName) go transcode(quality, outputQuality, cfg) } } // Convert video to ANSI text func transcode(input, output *messaging.Quality, cfg *Options) { // Start ffmpeg to transcode video to rawvideo videoInput := make(chan []byte, 1024) input.Register(videoInput) ffmpeg, rawvideo, err := startFFmpeg(videoInput, cfg) if err != nil { log.Printf("Error while starting ffmpeg: %s", err) return } // Transcode rawvideo to ANSI text pixelBuff := make([]byte, cfg.Width*cfg.Height) textBuff := bytes.Buffer{} for { n, err := (*rawvideo).Read(pixelBuff) if err != nil { log.Printf("An error occurred while reading input: %s", err) break } if n == 0 { // Stream is finished break } // Header textBuff.Reset() textBuff.Grow((40*cfg.Width+6)*cfg.Height + 47) for i := 0; i < 42; i++ { textBuff.WriteByte('\n') } // Convert image to ASCII for i, pixel := range pixelBuff { if i%cfg.Width == 0 { // New line textBuff.WriteString("\033[49m\n") } // Print two times the character to make a square text := fmt.Sprintf("\033[48;2;%d;%d;%dm ", pixel, pixel, pixel) textBuff.WriteString(text) textBuff.WriteString(text) } textBuff.WriteString("\033[49m") output.Broadcast <- textBuff.Bytes() } // Stop transcode ffmpeg.Process.Kill() } // Start a ffmpeg instance to convert stream into rawvideo func startFFmpeg(in <-chan []byte, cfg *Options) (*exec.Cmd, *io.ReadCloser, error) { bitrate := fmt.Sprintf("%dk", cfg.Width*cfg.Height*cfg.Framerate) ffmpegArgs := []string{"-hide_banner", "-loglevel", "error", "-i", "pipe:0", "-an", "-vf", fmt.Sprintf("scale=%dx%d", cfg.Width, cfg.Height), "-b:v", bitrate, "-minrate", bitrate, "-maxrate", bitrate, "-bufsize", bitrate, "-q", "42", "-pix_fmt", "gray", "-f", "rawvideo", "pipe:1"} ffmpeg := exec.Command("ffmpeg", ffmpegArgs...) // Handle errors output errOutput, err := ffmpeg.StderrPipe() if err != nil { return nil, nil, err } go func() { scanner := bufio.NewScanner(errOutput) for scanner.Scan() { log.Printf("[TELNET FFMPEG %s] %s", "demo", scanner.Text()) } }() // Handle text output output, err := ffmpeg.StdoutPipe() if err != nil { return nil, nil, err } // Handle stream input input, err := ffmpeg.StdinPipe() if err != nil { return nil, nil, err } go func() { for data := range in { input.Write(data) } }() // Start process err = ffmpeg.Start() return ffmpeg, &output, err }