Updated 2011-07-04 22:13:34 by RLE

EF The purpose of this page is to discuss ways to use the Skype API [1] from Tcl. Skype is a well-known IP telephony (and now video conferencing) application that runs on all major platforms. To make it short, the Skype API uses simple textual commands (initial keyword followed by (a number) of parameters to let external programs controlling Skype in various ways. How to send these commands is OS dependent, but uses the various message passing APIs that are offered by Windows, Linux and Mac OSX. This is explained further in the documentation linked above.

My first initial attempts have been on Windows. On Windows, external programs send commands to Skype through sending WM_COPYDATA messages to the Skype Window (and Skype will answer the same way). There are both synchronous and asynchronous commands and your program should be able to receive commands and information from Skype at any time. One good program for understanding the Skype API and running some example tests is SkypeTracer [2].

I could not find any easy way to send (and receive) WM_COPYDATA messages from Tcl, so I went for using an external component for controlling Skype. ActiveS [3] is an ActiveX layer around the Skype API that provides a higher level of abstraction. What I have succeeded in doing so far are simply a number of interactive proof-of-concept examples. ActiveS, once installed, makes available a COM object called SKYPEAPI. This object can easily be called from tcom. ActiveS comes with its own documentation, and most of its functionality is available through the Access class. The piece of code below, meant to be run line by line from a command prompt, will initiate connection to Skype, call me and hang up after a while.
 package require tcom
 # Get a reference to the Access class in the SKYPEAPI COM object installed by ActiveS
 set skype [::tcom::ref createobject "SKYPEAPI.Access"]
 # Connect to skype and wait for connection to succeed within 5 s.
 $skype ConnectAndWait 5000
 # Call a user (me but I'm off-line right now!)
 set call [$skype PlaceCall efrecon]
 # Hang up, when you don't want to talk to me anymore
 $call Status 7

Still, this relies on an external COM API and on an abstraction that perhaps is not suitable for Tcl. For example, ending a call through setting a call property to "7" feels a bit awkward. Perhaps is the raw API call through WM_COPYDATA messages a better approach anyhow.

EF In the process of trying to write a more generic library to access Skype, I discovered that the API did not have support to send the video window of the other party in fullscreen. There is no keyboard shortcut either. Consequently, I had no other choice than to send appropriate mouse events to appropriate windows to do the trick. The following (rather hugly) code will do the trick. It requires my newly written winapi (which really was written for that sole purpose).
 package require winapi
 
 set topwin [::winapi::FindWindows 0 "*MainForm*" "Skype*efrecon"]
 puts "TOP:$topwin: '[::winapi::GetClassName $topwin]' '[::winapi::GetWindowText $topwin]'"
 
 proc waitForWins { top class text { poll 500 } } {
     set wins ""
     while { $wins == "" } {
         set wins [::winapi::FindDescendantWindows $top $class $text]
         if { $wins == "" } {
             after $poll
         }
     }
 
     set allwins ""
     foreach w $wins {
         set rect [::winapi::GetWindowRect $w]
         foreach {left top right bottom} $rect {}
         lappend allwins $w $left $top $right $bottom
     }
     return $allwins
 }
 
 set video [waitForWins $topwin "tSkLocalVideoControl" ""]
 foreach {video left top right bottom} $video {}
 puts "Video is at : $video $left $top $right $bottom"
 set x [expr $left + (($right - $left) / 2)]
 set y [expr $top + (($bottom - $top) / 2)]
 set sx [::winapi::GetSystemMetrics SM_CXSCREEN]
 set sy [::winapi::GetSystemMetrics SM_CYSCREEN]
 ::winapi::SendMouseInput [expr {$x * 65535 / $sx}] [expr {$y *65535 / $sy}] 
     {ABSOLUTE MOVE}
 
 # Pick the button that is closest to the top of the screen and most to
 # the left (in *that* order, beware, otherwise we will get hold of the
 # video closing buttons that are hidden but in the hiearchy anyhow.
 
 # First build a list of all buttons.
 set allbuttons [waitForWins $video "tRegionButton" ""]
 
 # Second find the ones closest to the top of the screen
 set min_top ""
 set buttons ""
 foreach {w left top right bottom } $allbuttons {
     if { $min_top == "" || $top < $min_top } {
         set min_top $top
     }
 }
 foreach {w left top right bottom } $allbuttons {
     if { $top == $min_top } {
         lappend buttons $w $left $top $right $bottom
     }
 }
 
 # Third find the one mostly to the left.
 set min_left ""
 set button ""
 foreach {w left top right bottom } $buttons {
     if { $min_left == "" || $left < $min_left } {
         set min_left $left
     }
 }
 foreach {w left top right bottom } $buttons {
     if { $left == $min_left } {
         set button [list $w $left $top $right $bottom]
         break
     }
 }
 
 # Found, compute center of button, send the mouse there and simulate a click
 foreach {w left top right bottom } $button {}
 set bx [expr $left + (($right - $left) / 2)]
 set by [expr $top + (($bottom - $top) / 2)]
 
 ::winapi::SetWindowPos $topwin HWND_TOPMOST
 ::winapi::SendMouseInput [expr {$bx * 65535 / $sx}] [expr {$by *65535 / $sy}] 
     {ABSOLUTE MOVE LEFTDOWN LEFTUP}
 ::winapi::SetWindowPos $topwin HWND_NOTOPMOST
 ::winapi::SetWindowPos $topwin HWND_BOTTOM

msorc One may have a look at tclskype for simple interface from Unix or Windows (Mac OS X port is planned)

sbron On linux it's also possible to communicate with Skype through dbus, for example using dbus-tcl:
 package require dbus
 dbus connect

 # Setup a handler for messages from Skype
 dbus register /com/Skype/Client notify

 # Setup a handler for name change signals ...
 dbus register /org/freedesktop/DBus monitor
 # ... and ask the D-Bus server to notify us about name changes
 dbus filter add -type signal -path /org/freedesktop/DBus \
   -interface org.freedesktop.DBus -member NameOwnerChanged

 # Print a string on stdout with a timestamp in milliseconds
 proc ts {str} {
     set ts [clock milliseconds]
     set s [expr {$ts / 1000}]
     set ms [expr {$ts % 1000}]
     puts [format {%s.%03d: %s} [clock format $s -format %T] $ms $str]
 }

 # Send a command to Skype
 proc skypecmd {args} {
     ts [info level 0]
     set rc [dbus call -autostart no -dest com.Skype.API \
       /com/Skype com.Skype.API Invoke [join $args]]
     ts "=> $rc"
     return $rc
 }

 # Handle notify calls from Skype
 proc notify {info args} {
     ts [join $args]
 }

 # Connect to Skype
 proc connect {} {
     after cancel connect
     # We get an error if Skype is not running
     if {[catch {skypecmd NAME skype-tcl} result]} return
     if {$result eq "OK"} {
        # Skype is running and connected. Initiate communication.
        skypecmd PROTOCOL 7
        skypecmd GET SKYPEVERSION
     } else {
        # Skype is not yet ready. Try again a little later.
        after 500 connect
     }
 }

 proc monitor {info name old new} {
     if {$name eq "com.Skype.API"} {
        # As soon as Skype starts, try to connect
        if {$old eq ""} connect
        # Report when Skype terminates
        if {$new eq ""} {ts "Skype terminated"}
     }
 }

 connect
 vwait forever