Skip to content

Commit

Permalink
minor improvements to video helper and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
stakach committed Dec 18, 2022
1 parent e022eac commit 4f9fa75
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 22 deletions.
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
test.mp4


output.png
output.png
output2.png
output3.png
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,33 @@ Primarily to extract video frames from streams and files for processing by AI.

## Usage

See the specs for usage
parsing a video file, outputs [StumpyCore Canvas](https://github.com/stumpycr/stumpy_core#stumpy_core)

```crystal
require "ffmpeg"
video = Video::File.new("./test.mp4")
video.each_frame do |frame, is_key_frame|
frame # => StumpyCore::Canvas
end
```

also supports UDP streams (unicast or multicast)

```crystal
require "ffmpeg"
video = Video::UDP.new("udp://239.0.0.2:1234")
video.each_frame do |frame, is_key_frame|
frame # => StumpyCore::Canvas
end
```

See the specs for more detailed usage

## Contributing

Expand Down
7 changes: 7 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ dependencies:
stumpy_core:
github: stumpycr/stumpy_core

# used to check if an IP address is a multicast address
ipaddress:
github: Sija/ipaddress.cr

development_dependencies:
# we use stumpy to extract each pixels colour and draw bounding boxes
stumpy_png:
github: stumpycr/stumpy_png

ameba:
github: veelenga/ameba

authors:
- Stephen von Takach <[email protected]>

Expand Down
2 changes: 1 addition & 1 deletion spec/ffmpeg_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module FFmpeg

format.on_read do |bytes|
print "r"
bytes_read, client_addr = socket.receive(bytes)
bytes_read, _client_addr = socket.receive(bytes)
bytes_read
end
end
Expand Down
34 changes: 32 additions & 2 deletions spec/video_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ require "stumpy_png"

module FFmpeg
describe FFmpeg::Video do
Spec.before_each { File.delete?("./output.png") }
Spec.before_each do
File.delete?("./output.png")
File.delete?("./output2.png")
end

it "uses helpers to decode video frames" do
video = Video::File.new("./test.mp4")
Expand All @@ -20,8 +23,35 @@ module FFmpeg
break
end

video.close
File.exists?("./output.png").should be_true

# tests we can re-use the helper
write_frame = 200
frame_count = 0
video.each_frame do |frame|
frame_count += 1
next if frame_count < write_frame
puts "writing output"
StumpyPNG.write(frame, "./output2.png")
break
end

File.exists?("./output2.png").should be_true
end

it "works with streams" do
pending!("start a stream to test")
video = Video::UDP.new("udp://239.0.0.2:1234")
write_frame = 60
frame_count = 0
video.each_frame do |frame|
frame_count += 1
next if frame_count < write_frame
puts "writing output"
StumpyPNG.write(frame, "./output3.png")
break
end
File.exists?("./output3.png").should be_true
end
end
end
2 changes: 1 addition & 1 deletion src/ffmpeg/codec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class FFmpeg::Codec

def decode(packet : Packet) : Frame?
frame_finished = 0
bytes_allocated = LibAV::Codec.decode_video(@context, frame, pointerof(frame_finished), packet)
_bytes_allocated = LibAV::Codec.decode_video(@context, frame, pointerof(frame_finished), packet)
frame if frame_finished != 0
end

Expand Down
2 changes: 1 addition & 1 deletion src/ffmpeg/frame.cr
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class FFmpeg::Frame
@frame.value.format
end

def key_frame : Bool
def key_frame? : Bool
@frame.value.key_frame != 0
end

Expand Down
36 changes: 26 additions & 10 deletions src/ffmpeg/video.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "stumpy_core"
require "../ffmpeg"
require "ipaddress"
require "uri"

abstract class FFmpeg::Video
Expand All @@ -9,7 +10,8 @@ abstract class FFmpeg::Video

abstract def configure_read
abstract def input : String
abstract def format : Format

getter format : Format = Format.new

def close
@io.close
Expand Down Expand Up @@ -54,57 +56,71 @@ abstract class FFmpeg::Video
canvas.pixels[index] = StumpyCore::RGBA.new(r, g, b)
end

yield canvas
yield canvas, frame.key_frame?
end
end
end
end
ensure
close
@format = Format.new
GC.collect
end

class File < Video
def initialize(filename : String)
@input = filename
@format = Format.new
@io = ::File.open(filename)
end

getter input : String
getter format : Format
@io : ::File

def configure_read
Log.trace { "configuring IO callback" }
@io = ::File.open(input) if closed?
format.on_read { |bytes| @io.read(bytes) }
end
end

class UDP < Video
MULTICASTRANGEV4 = IPAddress.new("224.0.0.0/4")
MULTICASTRANGEV6 = IPAddress.new("ff00::/8")

def initialize(stream : String, read_buffer_size : Int = 1024 * 1024 * 4)
uri = URI.parse stream
@host = uri.host
@host = uri.host.as(String)
@port = uri.port || 554

@input = stream
@format = Format.new
@io = UDPSocket.new
@io.buffer_size = read_buffer_size
@io.read_buffering = true
@io.read_timeout = 2.seconds
end

getter input : String
getter format : Format
@io : UDPSocket
getter host : String
getter port : Int32

def configure_read
Log.trace { "connecting to stream" }
if closed?
read_buffer_size = @io.buffer_size
@io = UDPSocket.new
@io.buffer_size = read_buffer_size
@io.read_buffering = true
@io.read_timeout = 2.seconds
end

socket = @io

begin
ipaddr = IPAddress.new(@host, @port)
ipaddr = IPAddress.new(host)
if ipaddr.is_a?(IPAddress::IPv4) ? MULTICASTRANGEV4.includes?(ipaddr) : MULTICASTRANGEV6.includes?(ipaddr)
socket.bind "0.0.0.0", @port
socket.join_group(ipaddr)
socket.join_group(Socket::IPAddress.new(@host, @port))
else
socket.connect(@host, @port)
end
Expand All @@ -115,7 +131,7 @@ abstract class FFmpeg::Video

Log.trace { "configuring IO callback" }
format.on_read do |bytes|
bytes_read, client_addr = socket.receive(bytes)
bytes_read, _client_addr = socket.receive(bytes)
bytes_read
end
end
Expand Down

0 comments on commit 4f9fa75

Please sign in to comment.