Added getters for structs
This commit is contained in:
parent
fb2c0b201d
commit
4fef48055a
6 changed files with 257 additions and 126 deletions
123
README.md
123
README.md
|
@ -15,29 +15,24 @@ go get github.com/AlexEidt/Vidio
|
|||
The `Video` struct stores data about a video file you give it. The code below shows an example of sequentially reading the frames of the given video.
|
||||
|
||||
```go
|
||||
type Video struct {
|
||||
filename string // Video Filename
|
||||
width int // Width of Frames
|
||||
height int // Height of Frames
|
||||
depth int // Depth of Frames
|
||||
bitrate int // Bitrate for video encoding
|
||||
frames int // Total number of frames
|
||||
duration float64 // Duration in seconds
|
||||
fps float64 // Frames per second
|
||||
codec string // Codec used to encode video
|
||||
audio_codec string // Codec used for audio encoding
|
||||
pix_fmt string // Pixel format video is stored in
|
||||
framebuffer []byte // Raw frame data
|
||||
pipe *io.ReadCloser // Stdout pipe for ffmpeg process
|
||||
cmd *exec.Cmd // ffmpeg command
|
||||
}
|
||||
FileName() string
|
||||
Width() int
|
||||
Height() int
|
||||
Depth() int
|
||||
Bitrate() int
|
||||
Frames() int
|
||||
Duration() float64
|
||||
FPS() float64
|
||||
Codec() string
|
||||
AudioCodec() string
|
||||
FrameBuffer() string
|
||||
```
|
||||
|
||||
```go
|
||||
video := vidio.NewVideo("input.mp4")
|
||||
for video.Read() {
|
||||
// "frame" stores the video frame as a flattened RGB image
|
||||
frame := video.framebuffer // stored as: RGBRGBRGBRGB...
|
||||
// "frame" stores the video frame as a flattened RGB image in row-major order
|
||||
frame := video.FrameBuffer() // stored as: RGBRGBRGBRGB...
|
||||
// Video processing here...
|
||||
}
|
||||
```
|
||||
|
@ -47,17 +42,13 @@ for video.Read() {
|
|||
The `Camera` can read from any cameras on the device running Vidio. It takes in the stream index. On most machines the webcam device has index 0. Note that audio retrieval from the microphone is not yet supported.
|
||||
|
||||
```go
|
||||
type Camera struct {
|
||||
name string // Camera device name
|
||||
width int // Camera frame width
|
||||
height int // Camera frame height
|
||||
depth int // Camera frame depth
|
||||
fps float64 // Camera frames per second
|
||||
codec string // Camera codec
|
||||
framebuffer []byte // Raw frame data
|
||||
pipe *io.ReadCloser // Stdout pipe for ffmpeg process streaming webcam
|
||||
cmd *exec.Cmd // ffmpeg command
|
||||
}
|
||||
Name() string
|
||||
Width() int
|
||||
Height() int
|
||||
Depth() int
|
||||
FPS() float64
|
||||
Codec() string
|
||||
FrameBuffer() string
|
||||
```
|
||||
|
||||
```go
|
||||
|
@ -67,7 +58,7 @@ defer camera.Close()
|
|||
// Stream the webcam
|
||||
for camera.Read() {
|
||||
// "frame" stores the video frame as a flattened RGB image
|
||||
frame := camera.framebuffer // stored as: RGBRGBRGBRGB...
|
||||
frame := camera.FrameBuffer() // stored as: RGBRGBRGBRGB...
|
||||
// Video processing here...
|
||||
}
|
||||
```
|
||||
|
@ -77,35 +68,30 @@ for camera.Read() {
|
|||
The `VideoWriter` is used to write frames to a video file. The only required parameters are the output file name, the width and height of the frames being written, and an `Options` struct. This contains all the desired properties of the new video you want to create.
|
||||
|
||||
```go
|
||||
type Options struct {
|
||||
bitrate int // Bitrate
|
||||
loop int // For GIFs only. -1=no loop, 0=loop forever, >0=loop n times
|
||||
delay int // Delay for Final Frame of GIFs. Default -1 (Use same delay as previous frame)
|
||||
macro int // macro size for determining how to resize frames for codecs. Default 16
|
||||
fps float64 // Frames per second. Default 25
|
||||
quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
|
||||
codec string // Codec for video. Default libx264
|
||||
audio string // File path for audio for the video. If no audio, audio=nil.
|
||||
audio_codec string // Codec for audio. Default aac
|
||||
}
|
||||
FileName() string
|
||||
Width() int
|
||||
Height() int
|
||||
Bitrate() int
|
||||
Loop() int
|
||||
Delay() int
|
||||
Macro() int
|
||||
FPS() float64
|
||||
Quality() float64
|
||||
Codec() string
|
||||
AudioCodec() string
|
||||
```
|
||||
|
||||
```go
|
||||
type VideoWriter struct {
|
||||
filename string // Output filename
|
||||
audio string // Audio filename
|
||||
width int // Frame width
|
||||
height int // Frame height
|
||||
bitrate int // Output video bitrate
|
||||
loop int // Number of times for GIF to loop
|
||||
delay int // Delay of final frame of GIF. Default -1 (same delay as previous frame)
|
||||
macro int // Macroblock size for determining how to resize frames for codecs
|
||||
fps float64 // Frames per second for output video. Default 25
|
||||
quality float64 // Used if bitrate not given. Default 0.5
|
||||
codec string // Codec to encode video with. Default libx264
|
||||
audio_codec string // Codec to encode audio with. Default aac
|
||||
pipe *io.WriteCloser // Stdout pipe of ffmpeg process
|
||||
cmd *exec.Cmd // ffmpeg command
|
||||
type Options struct {
|
||||
Bitrate int // Bitrate
|
||||
Loop int // For GIFs only. -1=no loop, 0=loop forever, >0=loop n times
|
||||
Delay int // Delay for Final Frame of GIFs. Default -1 (Use same delay as previous frame)
|
||||
Macro int // macro size for determining how to resize frames for codecs. Default 16
|
||||
FPS float64 // Frames per second. Default 25
|
||||
Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
|
||||
Codec string // Codec for video. Default libx264
|
||||
Audio string // File path for audio for the video. If no audio, audio=nil.
|
||||
AudioCodec string // Codec for audio. Default aac
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -142,16 +128,16 @@ Copy `input.mp4` to `output.mp4`. Copy the audio from `input.mp4` to `output.mp4
|
|||
```go
|
||||
video := vidio.NewVideo("input.mp4")
|
||||
options := vidio.Options{
|
||||
fps: video.fps,
|
||||
bitrate: video.bitrate,
|
||||
audio: "input.mp4",
|
||||
FPS: video.FPS(),
|
||||
Bitrate: video.Bitrate(),
|
||||
Audio: "input.mp4",
|
||||
}
|
||||
|
||||
writer := vidio.NewVideoWriter("output.mp4", video.width, video.height, &options)
|
||||
defer writer.Close()
|
||||
|
||||
for video.Read() {
|
||||
writer.Write(video.framebuffer)
|
||||
writer.Write(video.FrameBuffer())
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -161,22 +147,23 @@ Grayscale 1000 frames of webcam stream and store in `output.mp4`.
|
|||
webcam := vidio.NewCamera(0)
|
||||
defer webcam.Close()
|
||||
|
||||
options := vidio.Options{fps: webcam.fps}
|
||||
options := vidio.Options{FPS: webcam.FPS()}
|
||||
|
||||
writer := vidio.NewVideoWriter("output.mp4", webcam.width, webcam.height, &options)
|
||||
defer writer.Close()
|
||||
|
||||
count := 0
|
||||
for webcam.Read() {
|
||||
for i := 0; i < len(webcam.framebuffer); i += 3 {
|
||||
rgb := webcam.framebuffer[i : i+3]
|
||||
frame := webcam.FrameBuffer()
|
||||
for i := 0; i < len(frame); i += 3 {
|
||||
rgb := frame[i : i+3]
|
||||
r, g, b := int(rgb[0]), int(rgb[1]), int(rgb[2])
|
||||
gray := uint8((3*r + 4*g + b) / 8)
|
||||
webcam.framebuffer[i] = gray
|
||||
webcam.framebuffer[i+1] = gray
|
||||
webcam.framebuffer[i+2] = gray
|
||||
frame[i] = gray
|
||||
frame[i+1] = gray
|
||||
frame[i+2] = gray
|
||||
}
|
||||
writer.Write(webcam.framebuffer)
|
||||
writer.Write(frame)
|
||||
count++
|
||||
if count > 1000 {
|
||||
break
|
||||
|
@ -189,7 +176,7 @@ Create a gif from a series of `png` files enumerated from 1 to 10 that loops con
|
|||
```go
|
||||
w, h, _ := vidio.Read("1.png") // Get frame dimensions from first image
|
||||
|
||||
options := vidio.Options{fps: 1, loop: -1, delay: 1000}
|
||||
options := vidio.Options{FPS: 1, Loop: -1, Delay: 1000}
|
||||
|
||||
gif := vidio.NewVideoWriter("output.gif", w, h, &options)
|
||||
defer gif.Close()
|
||||
|
|
28
camera.go
28
camera.go
|
@ -22,6 +22,34 @@ type Camera struct {
|
|||
cmd *exec.Cmd // ffmpeg command.
|
||||
}
|
||||
|
||||
func (camera *Camera) Name() string {
|
||||
return camera.name
|
||||
}
|
||||
|
||||
func (camera *Camera) Width() int {
|
||||
return camera.width
|
||||
}
|
||||
|
||||
func (camera *Camera) Height() int {
|
||||
return camera.height
|
||||
}
|
||||
|
||||
func (camera *Camera) Depth() int {
|
||||
return camera.depth
|
||||
}
|
||||
|
||||
func (camera *Camera) FPS() float64 {
|
||||
return camera.fps
|
||||
}
|
||||
|
||||
func (camera *Camera) Codec() string {
|
||||
return camera.codec
|
||||
}
|
||||
|
||||
func (camera *Camera) FrameBuffer() []byte {
|
||||
return camera.framebuffer
|
||||
}
|
||||
|
||||
// Returns the webcam device name.
|
||||
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
|
||||
func getDevicesWindows() []string {
|
||||
|
|
4
utils.go
4
utils.go
|
@ -116,8 +116,8 @@ func addVideoData(data map[string]string, video *Video) {
|
|||
if codec, ok := data["codec_name"]; ok {
|
||||
video.codec = codec
|
||||
}
|
||||
if pix_fmt, ok := data["pix_fmt"]; ok {
|
||||
video.pix_fmt = pix_fmt
|
||||
if pixfmt, ok := data["pix_fmt"]; ok {
|
||||
video.pixfmt = pixfmt
|
||||
}
|
||||
}
|
||||
|
||||
|
|
55
video.go
55
video.go
|
@ -18,13 +18,60 @@ type Video struct {
|
|||
duration float64 // Duration of video in seconds.
|
||||
fps float64 // Frames per second.
|
||||
codec string // Codec used for video encoding.
|
||||
audio_codec string // Codec used for audio encoding.
|
||||
pix_fmt string // Pixel format video is stored in.
|
||||
audioCodec string // Codec used for audio encoding.
|
||||
pixfmt string // Pixel format video is stored in.
|
||||
framebuffer []byte // Raw frame data.
|
||||
pipe *io.ReadCloser // Stdout pipe for ffmpeg process.
|
||||
cmd *exec.Cmd // ffmpeg command.
|
||||
}
|
||||
|
||||
func (video *Video) FileName() string {
|
||||
return video.filename
|
||||
}
|
||||
|
||||
func (video *Video) Width() int {
|
||||
return video.width
|
||||
}
|
||||
|
||||
func (video *Video) Height() int {
|
||||
return video.height
|
||||
}
|
||||
|
||||
// Channels of video frames.
|
||||
func (video *Video) Depth() int {
|
||||
return video.depth
|
||||
}
|
||||
|
||||
// Bitrate of video.
|
||||
func (video *Video) Bitrate() int {
|
||||
return video.bitrate
|
||||
}
|
||||
|
||||
// Total number of frames in video.
|
||||
func (video *Video) Frames() int {
|
||||
return video.frames
|
||||
}
|
||||
|
||||
func (video *Video) Duration() float64 {
|
||||
return video.duration
|
||||
}
|
||||
|
||||
func (video *Video) FPS() float64 {
|
||||
return video.fps
|
||||
}
|
||||
|
||||
func (video *Video) Codec() string {
|
||||
return video.codec
|
||||
}
|
||||
|
||||
func (video *Video) AudioCodec() string {
|
||||
return video.audioCodec
|
||||
}
|
||||
|
||||
func (video *Video) FrameBuffer() []byte {
|
||||
return video.framebuffer
|
||||
}
|
||||
|
||||
// Creates a new Video struct.
|
||||
// Uses ffprobe to get video information and fills in the Video struct with this data.
|
||||
func NewVideo(filename string) *Video {
|
||||
|
@ -41,8 +88,8 @@ func NewVideo(filename string) *Video {
|
|||
video := &Video{filename: filename, depth: 3}
|
||||
|
||||
addVideoData(videoData, video)
|
||||
if audio_codec, ok := audioData["codec_name"]; ok {
|
||||
video.audio_codec = audio_codec
|
||||
if audioCodec, ok := audioData["codec_name"]; ok {
|
||||
video.audioCodec = audioCodec
|
||||
}
|
||||
|
||||
return video
|
||||
|
|
135
videowriter.go
135
videowriter.go
|
@ -3,6 +3,7 @@ package vidio
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
|
@ -11,33 +12,77 @@ import (
|
|||
)
|
||||
|
||||
type VideoWriter struct {
|
||||
filename string // Output filename.
|
||||
audio string // Audio filename.
|
||||
width int // Frame width.
|
||||
height int // Frame height.
|
||||
bitrate int // Output video bitrate.
|
||||
loop int // Number of times for GIF to loop.
|
||||
delay int // Delay of final frame of GIF. Default -1 (same delay as previous frame).
|
||||
macro int // Macroblock size for determining how to resize frames for codecs.
|
||||
fps float64 // Frames per second for output video. Default 25.
|
||||
quality float64 // Used if bitrate not given. Default 0.5.
|
||||
codec string // Codec to encode video with. Default libx264.
|
||||
audio_codec string // Codec to encode audio with. Default aac.
|
||||
pipe *io.WriteCloser // Stdout pipe of ffmpeg process.
|
||||
cmd *exec.Cmd // ffmpeg command.
|
||||
filename string // Output filename.
|
||||
audio string // Audio filename.
|
||||
width int // Frame width.
|
||||
height int // Frame height.
|
||||
bitrate int // Output video bitrate.
|
||||
loop int // Number of times for GIF to loop.
|
||||
delay int // Delay of final frame of GIF. Default -1 (same delay as previous frame).
|
||||
macro int // Macroblock size for determining how to resize frames for codecs.
|
||||
fps float64 // Frames per second for output video. Default 25.
|
||||
quality float64 // Used if bitrate not given. Default 0.5.
|
||||
codec string // Codec to encode video with. Default libx264.
|
||||
audioCodec string // Codec to encode audio with. Default aac.
|
||||
pipe *io.WriteCloser // Stdout pipe of ffmpeg process.
|
||||
cmd *exec.Cmd // ffmpeg command.
|
||||
}
|
||||
|
||||
// Optional parameters for VideoWriter.
|
||||
type Options struct {
|
||||
bitrate int // Bitrate.
|
||||
loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops.
|
||||
delay int // Delay for final frame of GIFs.
|
||||
macro int // Macroblock size for determining how to resize frames for codecs.
|
||||
fps float64 // Frames per second for output video.
|
||||
quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst.
|
||||
codec string // Codec for video.
|
||||
audio string // File path for audio. If no audio, audio=nil.
|
||||
audio_codec string // Codec for audio.
|
||||
Bitrate int // Bitrate.
|
||||
Loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops.
|
||||
Delay int // Delay for final frame of GIFs.
|
||||
Macro int // Macroblock size for determining how to resize frames for codecs.
|
||||
FPS float64 // Frames per second for output video.
|
||||
Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst.
|
||||
Codec string // Codec for video.
|
||||
Audio string // File path for audio. If no audio, audio=nil.
|
||||
AudioCodec string // Codec for audio.
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) FileName() string {
|
||||
return writer.filename
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Width() int {
|
||||
return writer.width
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Height() int {
|
||||
return writer.height
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Bitrate() int {
|
||||
return writer.bitrate
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Loop() int {
|
||||
return writer.loop
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Delay() int {
|
||||
return writer.delay
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Macro() int {
|
||||
return writer.macro
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) FPS() float64 {
|
||||
return writer.fps
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Quality() float64 {
|
||||
return writer.quality
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Codec() string {
|
||||
return writer.codec
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) AudioCodec() string {
|
||||
return writer.audioCodec
|
||||
}
|
||||
|
||||
// Creates a new VideoWriter struct with default values from the Options struct.
|
||||
|
@ -49,38 +94,38 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video
|
|||
|
||||
writer.width = width
|
||||
writer.height = height
|
||||
writer.bitrate = options.bitrate
|
||||
writer.bitrate = options.Bitrate
|
||||
|
||||
// Default Parameter options logic from:
|
||||
// https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L268.
|
||||
|
||||
// GIF settings
|
||||
writer.loop = options.loop // Default to infinite loop.
|
||||
if options.delay == 0 {
|
||||
writer.loop = options.Loop // Default to infinite loop.
|
||||
if options.Delay == 0 {
|
||||
writer.delay = -1 // Default to frame delay of previous frame.
|
||||
} else {
|
||||
writer.delay = options.delay
|
||||
writer.delay = options.Delay
|
||||
}
|
||||
|
||||
if options.macro == 0 {
|
||||
if options.Macro == 0 {
|
||||
writer.macro = 16
|
||||
} else {
|
||||
writer.macro = options.macro
|
||||
writer.macro = options.Macro
|
||||
}
|
||||
|
||||
if options.fps == 0 {
|
||||
if options.FPS == 0 {
|
||||
writer.fps = 25
|
||||
} else {
|
||||
writer.fps = options.fps
|
||||
writer.fps = options.FPS
|
||||
}
|
||||
|
||||
if options.quality == 0 {
|
||||
if options.Quality == 0 {
|
||||
writer.quality = 0.5
|
||||
} else {
|
||||
writer.quality = options.quality
|
||||
writer.quality = math.Max(0, math.Min(options.Quality, 1))
|
||||
}
|
||||
|
||||
if options.codec == "" {
|
||||
if options.Codec == "" {
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".wmv") {
|
||||
writer.codec = "msmpeg4"
|
||||
} else if strings.HasSuffix(strings.ToLower(filename), ".gif") {
|
||||
|
@ -89,24 +134,24 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video
|
|||
writer.codec = "libx264"
|
||||
}
|
||||
} else {
|
||||
writer.codec = options.codec
|
||||
writer.codec = options.Codec
|
||||
}
|
||||
|
||||
if options.audio != "" {
|
||||
if !exists(options.audio) {
|
||||
panic("Audio file " + options.audio + " does not exist.")
|
||||
if options.Audio != "" {
|
||||
if !exists(options.Audio) {
|
||||
panic("Audio file " + options.Audio + " does not exist.")
|
||||
}
|
||||
|
||||
if len(ffprobe(options.audio, "a")) == 0 {
|
||||
panic("Given \"audio\" file " + options.audio + " has no audio.")
|
||||
if len(ffprobe(options.Audio, "a")) == 0 {
|
||||
panic("Given \"audio\" file " + options.Audio + " has no audio.")
|
||||
}
|
||||
|
||||
writer.audio = options.audio
|
||||
writer.audio = options.Audio
|
||||
|
||||
if options.audio_codec == "" {
|
||||
writer.audio_codec = "aac"
|
||||
if options.AudioCodec == "" {
|
||||
writer.audioCodec = "aac"
|
||||
} else {
|
||||
writer.audio_codec = options.audio_codec
|
||||
writer.audioCodec = options.AudioCodec
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,7 +240,7 @@ func initVideoWriter(writer *VideoWriter) {
|
|||
if writer.audio != "" && !gif {
|
||||
command = append(
|
||||
command,
|
||||
"-acodec", writer.audio_codec,
|
||||
"-acodec", writer.audioCodec,
|
||||
"-map", "0:v:0",
|
||||
"-map", "1:a:0",
|
||||
)
|
||||
|
|
|
@ -25,8 +25,8 @@ func TestVideoMetaData(t *testing.T) {
|
|||
assertEquals(video.duration, 3.366667)
|
||||
assertEquals(video.fps, float64(30))
|
||||
assertEquals(video.codec, "h264")
|
||||
assertEquals(video.audio_codec, "aac")
|
||||
assertEquals(video.pix_fmt, "yuv420p")
|
||||
assertEquals(video.audioCodec, "aac")
|
||||
assertEquals(video.pixfmt, "yuv420p")
|
||||
assertEquals(len(video.framebuffer), 0)
|
||||
|
||||
if video.pipe != nil {
|
||||
|
@ -63,17 +63,17 @@ func TestVideoWriting(t *testing.T) {
|
|||
testWriting := func(input, output string, audio bool) {
|
||||
video := NewVideo(input)
|
||||
options := Options{
|
||||
fps: video.fps,
|
||||
bitrate: video.bitrate,
|
||||
codec: video.codec,
|
||||
FPS: video.FPS(),
|
||||
Bitrate: video.Bitrate(),
|
||||
Codec: video.Codec(),
|
||||
}
|
||||
if audio {
|
||||
options.audio = input
|
||||
options.Audio = input
|
||||
}
|
||||
|
||||
writer := NewVideoWriter(output, video.width, video.height, &options)
|
||||
for video.Read() {
|
||||
writer.Write(video.framebuffer)
|
||||
writer.Write(video.FrameBuffer())
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
|
@ -86,6 +86,30 @@ func TestVideoWriting(t *testing.T) {
|
|||
fmt.Println("Video Writing (without Audio) Test Passed")
|
||||
}
|
||||
|
||||
func TestCameraIO(t *testing.T) {
|
||||
webcam := NewCamera(0)
|
||||
|
||||
options := Options{FPS: webcam.FPS()}
|
||||
|
||||
writer := NewVideoWriter("test/camera.mp4", webcam.width, webcam.height, &options)
|
||||
|
||||
count := 0
|
||||
for webcam.Read() {
|
||||
frame := webcam.FrameBuffer()
|
||||
writer.Write(frame)
|
||||
count++
|
||||
if count > 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
webcam.Close()
|
||||
writer.Close()
|
||||
|
||||
//os.Remove("test/camera.mp4")
|
||||
fmt.Println("Camera IO Test Passed")
|
||||
}
|
||||
|
||||
func TestFFprobe(t *testing.T) {
|
||||
koalaVideo := ffprobe("test/koala.mp4", "v")
|
||||
assertEquals(koalaVideo["width"], "480")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue