Добро пожаловать! Войти Зарегистрироваться

Расширенный

Простой текстовый видео плеер

Написал w-495 
Простой текстовый видео плеер
18 Май 2015 01:01
Пример простого видео-плеера на Python, с использованием PyAV обертки для ffmpeg и libav.
https://gist.github.com/w495/173534c1de5e969cbd7e



#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
    Простой текстовый консольный видео-плеер.
    Выводит кадры в символьном представлении на стандартный вывод.
    Поддержки звука нет.

    Как пользоваться:

        $> simple-console-video-player.py /путь/до/видео-файла.mp4

'''

import sys

##
## Импортируем библиотеку PyAV (http://mikeboers.github.io/PyAV/).
## Библиотека была установлено командой
##      $> conda install -c danielballan pyav.
##

import av
from av.video.frame import VideoFrame

##
## Масштаб видео.
##
SCALE_MEASURE  = 0.5

##
## Формат видео.
## В данном случае оттенки серого цвета от 0 до 2¹⁶ (65536).
##
FORMAT = 'gray16'
FORMAT_GRAY16_SIZE = 65536

##
## Представление пикселей с помощью символов псевдографики.
##
TEXT_PIXEL_LIST = u' .-+#'

##
## Видео-файл по-умолчанию.
##
DEFAULT_FILE_NAME = 'video1.mp4'

def main():
    ## Получаем имя видео-файла.
    video_file_name = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_FILE_NAME
    ## Открываем его как видео-контейнер.
    video_container = av.open(video_file_name)
    ## Извлекаем пакеты из видео-контейнера — получаем список.
    packet_list = video_container.demux()
    ## Выполняем для каждого пакета.
    for packet in packet_list:
        # Получаем список кадров из пакета.
        frame_list = packet.decode()
        ## Выполняем для каждого кадра из списка.
        for frame in frame_list:
            ## Если кадры — это видео
            ## (а могут быть еще аудио, например).
            if(type(frame) == VideoFrame):
                new_width  = int(SCALE_MEASURE * frame.width)
                new_height = int(SCALE_MEASURE * frame.height)
                ## Создаем временную кадр-структуру в нужном нам формате.
                tmp_frame = frame.reformat(
                    width  = new_width,
                    height = new_height,
                    format = FORMAT
                )

                ## Получаем из временного кадра двумерный массив пикселей.
                nd_array = tmp_frame.to_nd_array()
                ## Печатаем массив точек.
                for nd_array1 in nd_array:
                    for x in nd_array1:
                        ## Рисуем пиксель в консоль без перевода строки.
                        print get_gray16_text_pixel(x),
                    ## Строка пикселей кончилась,
                    ## делаем перевод строки.
                    print '\n'
                ## Конец кадра рисуем ограничитель
                print '\n\n'
                print '==' * (new_width)
                print '\n\n'

    print 'Fin!'

def get_gray16_text_pixel(pixel):
    '''
        Возвращает символ-пиксель, который соответствует,
        настоящему пикселю-числу формата «gray16».
    '''
    ## Получаем размер диапазона оттенков,
    ## который будет представлен одним нашим текстовым «пикселем»
    step = FORMAT_GRAY16_SIZE / len(TEXT_PIXEL_LIST)
    ## Вычисляем номер нашего текстового пикселя.
    text_pixel_index = pixel / step
    ## Возвращаем символ из списка текстовых «пикселей»
    return TEXT_PIXEL_LIST[text_pixel_index]

if (__name__ == '__main__'):
    main()

Для сравнения аналогичный пример на С++, c интересным жонглированием указателями:
https://www.ffmpeg.org/doxygen/2.2/filtering_video_8c-example.html

Очередной раз показывает, в чем преимущество Python над C++.



Исправлений: 3. Последний раз редактировал w-495 в 18.05.2015, 01:01.
Вложения:
открыть | скачать - simple-console-video-player.png (202,4 KB)
открыть | скачать - simple-console-video-player-800.png (261,6 KB)
Re: Простой текстовый видео плеер
01 Июнь 2015 00:12
Пример простого видео-плеера на Go, с использованием Gmf.
https://gist.github.com/w495/1a40a09bfe50b8841602

Работа с видео на Go с библиотекой Gmf оказалась более гибкой.
Местами пришлось погрузиться в детали реализации, что сделало задачу написания простого приложения сложнее.

//     Простой текстовый консольный видео-плеер.
//     Выводит кадры в символьном представлении на стандартный вывод.
//     Поддержки звука нет.
//
//     Как пользоваться:
//
//         $> go run simple-console-video-player.go /путь/до/видео-файла.mp4
//

package main

/*
#cgo pkg-config: libavcodec libavutil

// Импортируем данные из заголовочных файлов Cи для ffmpeg и libgmf.
// Далее будем обращаться к ним, через объект «C».

#include "libavcodec/avcodec.h"

*/
import "C"

import (
    "fmt"
    "os"
    "runtime/debug"
    "github.com/3d0c/gmf"
)

// Опишем константы
const (
    // Формат видео. Оттенки серого цвета от 0 до 2¹⁶ (65536).
    FORMAT              int32  = C.AV_PIX_FMT_GRAY16
    GRAY16_SIZE         int    = 65536
    // Кодек. Никакого кодека мы тут использовать не будем.
    CODEC_ID            int    = C.AV_CODEC_ID_RAWVIDEO
    // Представление пикселей с помощью символов псевдографики.
    TEXT_PIXEL_LIST     string = " .-+#"
    // Масштаб видео (делим на эти число).
    SCALE_FACTOR        int    = 4
    DEFAULT_FILE_NAME   string = "tests-sample.mp4"
)

func main() {
    srcFileName := DEFAULT_FILE_NAME
    if len(os.Args) > 1 {
        // Получаем имя видео-файла.
        // Если нам его не передали как аргумент командной строки,
        // то берем имя файла по-умолчанию.
        srcFileName = os.Args[1]
    }
    // Открываем его как видео-контейнер.
    inputContext, error := gmf.NewInputCtx(srcFileName)
    if error != nil {
        fatal(error)
    }
    // Говорим, что хотим закрыть контейнер,
    // когда закончим работу с ним
    defer inputContext.CloseInputAndRelease()
    // Выбираем видео-поток из контейнера.
    srcVideoStream, error := inputContext.GetBestStream(gmf.AVMEDIA_TYPE_VIDEO)
    if error != nil {
        fmt.Println("No video stream found in", srcFileName)
    }
    // Получим контекст (найтройки) кодека для потока исходного файла
    srcCodecContext := srcVideoStream.CodecCtx()
    // Получим размеры кадров для исходного файла.
    srcWidth  := srcCodecContext.Width()
    srcHeight := srcCodecContext.Height()
    // Вычислим размеры кадра, нужные нам.
    dstWidth  := srcWidth  / SCALE_FACTOR
    dstHeight := srcHeight / SCALE_FACTOR
    // Найдем нужный нам кодек (AV_CODEC_ID_RAWVIDEO).
    codec, error := gmf.FindEncoder(CODEC_ID)
    if error != nil {
        fatal(error)
    }
    // Создадим контекст кодека, и опишем его парпметры.
    dstCodecContext := gmf.NewCodecCtx(codec).
        SetPixFmt(FORMAT).
        SetWidth(dstWidth).
        SetHeight(dstHeight)
    // Говорим, что хотим освобидить память,
    // когда закончим работу с dstCodecContext.
    defer gmf.Release(dstCodecContext)
    // Иницализируем (откроем) контекст кодека.
    if error := dstCodecContext.Open(nil); error != nil {
        fatal(error)
    }
    // Создадим контекст масштаба.
    // Зададим исходный и результрующий контексты кодеков,
    // и то, как будем приводить один к другому (SWS_BICUBIC).
    scaleContext := gmf.NewSwsCtx(srcCodecContext,
                                  dstCodecContext,
                                  gmf.SWS_BICUBIC)
    // Говорим, что хотим освобидить память,
    // когда закончим работу с dstCodecContext.
    defer gmf.Release(scaleContext)
    // Создадим новый кадр и опишем его параметры:
    //  цветовую модель (FORMAT) и размеры.
    dstFrame := gmf.NewFrame().
        SetWidth(dstWidth).
        SetHeight(dstHeight).
        SetFormat(FORMAT)
    // Говорим, что хотим освобидить память,
    // когда закончим работу с dstFrame.
    defer gmf.Release(dstFrame)
    // Иницализируем кадр — выделяем для него память.
    if error := dstFrame.ImgAlloc(); error != nil {
        fatal(error)
    }
    // Извлекаем пакеты из видео-контейнера и проходим по кажому из них.
    for packet := range inputContext.GetNewPackets() {
        if packet.StreamIndex() != srcVideoStream.Index() {
            // Пропускаем не-видео кадры
            // (могут быть еще аудио-кадры или субтитры).
            continue
        }
        // Получаем поток пакета.
        packetStream, error := inputContext.GetStream(packet.StreamIndex())
        if error != nil {
            fatal(error)
        }
        // Получаем список кадров из пакета,
        // выполняем для каждого кадра из списка.
        for frame := range packet.Frames(packetStream.CodecCtx()) {
            // Масштабируем исходный кадр и результат кладем в dstFrame.
            scaleContext.Scale(frame, dstFrame)
            // Для полученного кадра пытаемя выделить последовательность байт,
            // и вывести их на консоль.
            if p, ready, _ := dstFrame.EncodeNewPacket(dstCodecContext); ready {
                // Выводим кадр в консоль.
                writeFrame(p.Data(), dstFrame.Width())
                defer gmf.Release(p)
            }
        }
        // Освобождаем память из под packet.
        gmf.Release(packet)
    }
}

// Выводит в консоль один кадр видео.
// Важный момент, что на вход функция принимает набор байтов,
// а не пикселей. При FORMAT = AV_PIX_FMT_GRAY16,
// один пиксель равен двум байтам.
func writeFrame(byteLinputStream []byte, width int) {
    pixel := 0
    for index, item := range byteLinputStream {
        // Формируем пиксель из текущего байта и предыдущего.
        pixel = 256 * pixel + int(item)
        if 1 == index % 2 {
            // Рисуем пиксель в консоль без перевода строки.
            fmt.Print(getGray16TextPixel(pixel))
            index = index / 2
            if 0 == (index + 1) % width {
                // Строка пикселей кончилась,
                // делаем перевод строки.
                fmt.Println()
            }
            pixel = 0
        }
    }
    // Конец кадра рисуем ограничитель (перевод строки)
    fmt.Println()
}

// Возвращает символ-пиксель, который соответствует,
// настоящему пикселю-числу формата «gray16».
func getGray16TextPixel(pixel int) string {
    // Получаем размер диапазона оттенков,
    // который будет представлен одним нашим текстовым «пикселем».
    // Эту переменную можно вынести из функции для повышения эффективности.
    step := int(GRAY16_SIZE) / len(TEXT_PIXEL_LIST)
    // Вычисляем номер нашего текстового пикселя.
    text_pixel_index := pixel / (step + 1)
    // Возвращаем символ из списка текстовых «пикселей»
    text_pixel := string(TEXT_PIXEL_LIST[text_pixel_index])
    return text_pixel
}

// Выводит подробный отчет об ошибке
func fatal(err error) {
    debug.PrintStack()
    fmt.Println(err)
}



Исправлений: 2. Последний раз редактировал w-495 в 01.06.2015, 00:12.
К сожалению, только зарегистрированные пользователи могут писать в этом форуме.

Авторизоваться на форуме