- Allow switching between the two servers.
- Reconnect (after a random backoff time) when the stream breaks down.
- Save the images (either all or selected ones on a button click).
- Keep a history of the last N images so that the user can go back.
- Improve header handling (e.g. reading the boundary string from the header instead of assuming its value)
- Improve error handling.
- Add proxy support.
- Use the tcl http package instead of talking to the raw socket (not sure if that is possible with the multipart/x-mixed-replace content).
package require Tk
package require img::jpeg
image create photo foo -width 800 -height 600
pack [label .l -image foo]
proc READ {fd} {
global state toread frame
if {[eof $fd]} {
puts "stream closed by peer"
close $fd
set state ""
after 9000 start
}
switch -- $state {
response {
gets $fd line
puts "RESPONSE: $line"
if {$line ne "HTTP/1.0 200 OK"} exit
set state header
}
header {
gets $fd line
puts "HEADER: $line"
if {$line eq ""} {
set state boundary
}
}
boundary {
gets $fd line
if {$line eq "--myboundary"} {
set state mime
}
}
mime {
gets $fd line
puts "MIME: $line"
regexp {Content-Length: ([[:digit:]]+)} $line -> toread
if {$line eq ""} {
fconfigure $fd -translation binary
set state data
}
}
data {
set n [expr { $toread > 1000 ? 1000 : $toread }]
set data [read $fd $n]
incr toread -[string length $data]
append frame $data
if {$toread == 0} {
foo configure -data $frame
set frame ""
set state boundary
fconfigure $fd -translation crlf
}
}
}
}
proc start {} {
puts "opening stream"
global state frame toread
set toread 1000
set state response
set frame ""
set fd [socket eu.tclers.tk 80]
# set fd [socket us.tclers.tk 80]
fconfigure $fd -buffering full -translation crlf
puts $fd "GET /video.mjpg HTTP/1.0"
puts $fd ""
flush $fd
fileevent $fd readable [list READ $fd]
}
start
vwait foreverRZ changed order of statements because of runtime error (proc READ not found)Zarutian: added simple eof handlingZarutian: it now restarts on eof
EG: Here's my not so quick but still dirty version. It tries to implement the list of improvements suggested by rmax in his original version. It also uses coroutines for handling the incoming data in a non-blocking manner. Still missing proxy support.
package require Tcl 8.6 ;# coroutines
package require Tk
catch {package require Img}
package require img::jpeg
package require uri ;# tcllib
# helper procs to ease the creation of callbacks/gui
proc my {cmd args} {
linsert $args 0 [uplevel 1 [list namespace which $cmd]]
}
proc myvar {varname} {
uplevel 1 [list namespace which -variable $varname]
}
proc schedule {cmd args} {
after idle [list after 0 [uplevel 1 [linsert $args 0 my $cmd]]]
}
proc errCondition {msg} {
schedule tk_messageBox \
-type ok -icon error \
-title "Error" -message $msg
}
proc scroll {scroll from to} {
if {$from == 0 && $to == 1.0} {
if {[winfo ismapped $scroll]} {
grid remove $scroll
}
} else {
if {![winfo ismapped $scroll]} {
grid $scroll
}
}
$scroll set $from $to
}
# rmax's TODO
# * Allow switching between the two servers. DONE
# * Reconnect (after a random backoff time) when the stream breaks down.
# DONE (well, almost, you can reconnect manually)
# * Save the images (either all or selected ones on a button click). DONE
# * Keep a history of the last N images so that the user can go back. DONE
# * Improve header handling (e.g. reading the boundary string from
# the header instead of assuming its value). DONE
# * Improve error handling. DONE (kinda)
# * Add proxy support. STILL TODO
# * Use the tcl http package instead of talking to the raw socket
# (not sure if that is possible with the multipart/x-mixed-replace content).
#
namespace eval vfeed {
variable img [image create photo]
variable img_work [image create photo]
variable chunk 4096
# add some urls for the address bar.
# non-tcl ones taken from http://www.opentopia.com/hiddencam.php
variable urls {
http://us.tclers.tk/video.mjpg
http://eu.tclers.tk/video.mjpg
http://sjryc.axiscam.net/axis-cgi/mjpg/video.cgi
http://66.172.250.133/axis-cgi/mjpg/video.cgi
http://62.177.139.136:8088/axis-cgi/mjpg/video.cgi?camera=1&resolution=384x288
}
variable url [lindex $urls 0] ;# the value of address combobox
variable zoom 1 ;# image zoom
variable button ;# connect/disconnect button
variable canvas ;# image display canvas
variable dim {0 0} ;# image dimensions (cached)
variable tv ;# the treeview offline selector
variable save 0 ;# to save or not to save ...
variable home [file join [file normalize ~] videofeed]
file mkdir $home
}
proc vfeed::getline {fd} {
while {[chan gets $fd line] < 0} {
if {[chan eof $fd]} {
errCondition "Socket closed by peer"
disconnect $fd
return -code return
}
yield
}
return $line
}
proc vfeed::readbytes {fd bytes} {
variable chunk
set total {}
while 1 {
set n [expr { $bytes > $chunk ? $chunk : $bytes }]
set data [chan read $fd $n]
if {[chan eof $fd]} {
errCondition "Socket closed by peer"
disconnect $fd
return -code return
}
incr bytes -[string length $data]
append total $data
if {$bytes > 0} {
yield
} else {
break
}
}
return $total
}
# handle the status response from the server
proc vfeed::response {fd} {
yield [info coroutine]
set resp [getline $fd]
if {$resp ne "HTTP/1.0 200 OK"} {
errCondition "Error:\nServer respond:\n$resp"
disconnect $fd
return
}
chan event $fd readable [coroutine listener headers $fd]
}
# handle headers
proc vfeed::headers {fd} {
yield [info coroutine]
set headers {}
while {[set resp [getline $fd]] ne ""} {
lassign [split $resp ":"] key value
dict set headers $key [string trim $value]
}
regexp {boundary=([^[:space:]]+)} [dict get $headers "Content-Type"] -> boundary
chan event $fd readable [coroutine listener handler $fd $boundary]
}
# handle the data stream
proc vfeed::handler {fd boundary} {
yield [info coroutine]
variable img
variable img_work
variable zoom
# save the value of channel encoding
set encoding [chan configure $fd -encoding]
# clean the junk which may arrive before the boundary line
while {[set resp [getline $fd]] ne $boundary
&& $resp ne "--$boundary"} {}
while 1 {
set headers {}
while {[set resp [getline $fd]] ne ""} {
lassign [split $resp ":"] key value
dict set headers $key [string trim $value]
}
# check the content type
if {[dict get $headers Content-Type] ne "image/jpeg"} {
errCondition "Error:\nNot a jpeg stream"
break
}
# check the content length
if {![dict exists $headers Content-Length]} {
errCondition "Error:\nContent length missing"
break
} else {
set toread [dict get $headers Content-Length]
}
# now we are ready to receive the jpeg binary data.
chan configure $fd -translation binary -encoding binary
set frame [readbytes $fd $toread]
# display the new image
$img_work configure -data $frame
$img copy $img_work -subsample $zoom -shrink
newimage
# ready to start a new cycle
chan configure $fd -translation crlf -encoding $encoding
getline $fd
# let the event loop process idle events (display the image)
chan event $fd readable {}
schedule chan event $fd readable [info coroutine]
yield
# read the boundary line
if {[set resp [getline $fd]] ne $boundary && $resp ne "--$boundary"} {
errCondition "Error:\nexpected \"$boundary\", got \"$resp\""
break
}
}
disconnect $fd
}
# build the gui
proc vfeed::gui {} {
variable img
variable button
wm state . withdrawn
# a toolbar frame
set tb [ttk::frame .toolbar]
# the address combobox
set addr [ttk::combobox $tb.address \
-textvariable [myvar url]]
$addr configure -postcommand [my onPost $addr]
# the connect/disconnect button
set button [ttk::button $tb.switch -text "Connect" \
-command [linsert [my connect] 0 schedule] ]
# the save button
set sb [ttk::checkbutton $tb.save -text Save \
-variable [myvar save] \
-onvalue 1 -offvalue 0 \
-command [my loadfiles]]
# fill the toolbar
grid $addr $button $sb -sticky ew -pady 3 -padx 3
grid columnconfigure $tb $addr -weight 1
set pw [ttk::panedwindow .pw -orient horizontal]
set df [imgdisplay $pw $img]
set ov [imgselector $pw]
$pw add $df -weight 1
$pw add $ov -weight 0
pack $tb -fill x
pack $pw -expand 1 -fill both
bind all <Double-Escape> exit
wm title . "Video feed"
schedule wm state . normal
}
# creates a widget to display a photo image
proc vfeed::imgdisplay {parent img {width 640} {height 480}} {
variable canvas
# the display control
set df [ttk::frame $parent.df]
set c [canvas $df.canvas \
-bg white -borderwidth 0 \
-width $width -height $height]
set sx [ttk::scrollbar $df.sx \
-orient horizontal -command [list $c xview]]
set sy [ttk::scrollbar $df.sy \
-orient vertical -command [list $c yview]]
$c configure \
-xscrollcommand [my scroll $sx] \
-yscrollcommand [my scroll $sy]
grid $c $sy -sticky news
grid $sx -sticky ew
grid remove $sx $sy
grid columnconfigure $df $c -weight 1
grid rowconfigure $df $c -weight 1
grid remove $sx $sy
$c create image {0 0} -image $img -anchor nw
bind $c <ButtonPress-1> {%W scan mark %x %y}
bind $c <Button1-Motion> {%W scan dragto %x %y 2}
# add a control menu
set pm [menu $c.theme -tearoff 0]
# the zoom control
$pm add radiobutton -label "Zoom 100%" \
-variable [myvar zoom] \
-value 1
$pm add radiobutton -label "Zoom 50%" \
-variable [myvar zoom] \
-value 2
# a theme selector
$pm add separator
foreach theme [ttk::themes] {
$pm add command \
-label [string totitle $theme] \
-command [list ttk::setTheme $theme]
}
bind $c <Button-3> [list tk_popup $pm %X %Y]
bind all <Control-Key-1> [list set [myvar zoom] 1]
bind all <Control-Key-2> [list set [myvar zoom] 2]
set canvas $c
trace add execution $img leave [my imagechanged]
return $df
}
# the offline image selector
proc vfeed::imgselector {parent} {
variable img
variable tv
set t [ttk::frame $parent.viewer]
set collist {file size}
set colnames {Filename Size}
set colsizes {170 60}
set tv [ttk::treeview $t.tv \
-columns $collist \
-show headings \
-height 15 \
-yscrollcommand [my scroll $t.sy]\
-xscrollcommand [my scroll $t.sx]]
set sy [ttk::scrollbar $t.sy -orient vertical -command [list $tv yview]]
set sx [ttk::scrollbar $t.sx -orient horizontal -command [list $tv xview]]
foreach c $collist n $colnames s $colsizes {
$tv heading $c -text $n
$tv column $c -width $s -stretch 0
}
grid $tv $sy -sticky news
grid $sx -sticky ew
grid rowconfigure $t $tv -weight 1
grid columnconfigure $t $tv -weight 1
grid remove $sx $sy
grid propagate $t 0
bind $tv <<TreeviewSelect>> [my updateimg]
bind $tv <Key-Delete> [my deletefile]
event add <<SelectAll>> <Control-a> <Control-A>
bind $tv <<SelectAll>> {%W selection set [%W children {}]}
loadfiles
return $t
}
# Saves the image. Called when a new image arrives
proc vfeed::newimage {} {
variable home
variable img
variable save
variable tv
if {!$save} {
return
}
set base [clock format [clock seconds] \
-format %Y%m%d_%H%M%S \
-timezone :GMT]
set base [file join $home $base]
while {[file exists [set fname "${base}_[incr i].jpg"]]} {}
$img write $fname -format jpeg
$tv insert {} end -values [list [file tail $fname] [file size $fname]]
}
# Select an image to display. Called from the offline selector
proc vfeed::updateimg {} {
variable img
variable tv
variable home
set item [$tv selection]
if {[llength $item] != 1} {
return
}
set fname [$tv set $item file]
try {
$img read [file join $home $fname] -shrink
} trap {POSIX ENOENT} {} {
errCondition "No such file"
loadfiles
} trap {NONE} {} {
errCondition "Image format not recognized"
}
}
# trace procedure to update the scroll region on the display canvas
proc vfeed::imagechanged {args} {
variable canvas
variable img
variable dim
lassign $dim w h
set nw [image width $img]
set nh [image height $img]
if {($w != $nw) || ($h != $nh)} {
$canvas configure -scrollregion [list 0 0 $nw $nh]
set dim [list $nw $nh]
}
}
# update the list of urls in the combobox
proc vfeed::onPost {combo} {
variable urls
$combo configure -values $urls
}
# connect to the stream and disables the offline mode
proc vfeed::connect {} {
variable url
variable urls
variable button
set udict [uri::split $url]
dict with udict {
if {$port eq ""} {
set port 80
}
if {$query ne ""} {
append path ? $query
}
}
if {[catch {socket $host $port} fd]} {
errCondition "Error opening socket\n$fd"
return
}
chan configure $fd -buffering full -translation crlf -blocking 0
chan puts $fd "GET /$path HTTP/1.0\n"
chan flush $fd
chan event $fd readable [coroutine listener response $fd]
$button configure -text Disconnect -command [my disconnect $fd]
togglestate
if {$url ni $urls} {
lappend urls $url
}
}
# disconnect the stream and enter offline mode
proc vfeed::disconnect {fd} {
variable button
chan close $fd
# clean leftovers from handlers
rename [my listener] {}
foreach afterid [after info] {
set script [lindex [after info $afterid] 0]
if {[string match {*chan event*} $script]} {
after cancel $afterid
}
}
# reconfigure the button
$button configure -text Connect \
-command [linsert [my connect] 0 schedule]
togglestate
}
# load the list of files in the offline selector
proc vfeed::loadfiles {} {
coroutine loader apply [list {} {
variable tv
variable home
$tv delete [$tv children {}]
set i 0
foreach f [lsort [glob -nocomplain -directory $home *jpg]] {
set ft [file tail $f]
$tv insert {} end -values [list $ft [file size $f]]
incr i
if {($i % 10) == 0} {
schedule [info coroutine]
yield
}
}
} [namespace current]]
}
# enable/disable the selection on the offline selector
proc vfeed::togglestate {} {
variable tv
if {[$tv cget -selectmode] eq "none"} {
$tv configure -selectmode extended
loadfiles
} else {
$tv selection remove [$tv selection]
$tv configure -selectmode none
}
}
# delete the currently selected file(s)
proc vfeed::deletefile {} {
variable tv
variable home
if {([$tv cget -selectmode] eq "none")
||
([llength [set items [$tv selection]]] == 0)
} then {
return
}
foreach item $items {
set fname [$tv set $item file]
set fname [file join $home $fname]
if {[file exists $fname]} {
file delete $fname
}
$tv delete $item
}
}
# start the app
vfeed::gui
