Version 2 of VLC Tcl Extension

Updated 2016-12-29 15:12:08 by bll

VLC Tcl Extension

bll 2016-12-28 : One full day's work. This is a Tcl interface to libvlc, which comes with the VLC media player. The tighter interface gives better control over song position, time position and player state. In addition to some of the basic controls, I implemented a callback registration that will call a tcl procedure upon entering a particular player state.

I only need audio, and don't use playlists. The media sub-command only handles file paths right now. There's quite a bit that could still be implemented. But most of that implementation would be fairly straightforward.

It seems to be working on Linux and Mac OS X. It was crashing on Windows, but I believe that is fixed now (I certainly don't understand these reference count problems).

test.tcl

#!/usr/bin/tclsh

set os $::tcl_platform(os)
if { [regexp -nocase {^win} $os] } {
  set os Windows
}

load [pwd]/$os/tclvlc64[info sharedlibextension]
tclvlc init --intf dummy --no-video --ignore-config --no-loop --no-random --no-repeat --quiet --play-and-stop
if { $os eq "Linux" } {
  tclvlc media /home/bll/sources/ballroomdj/test.dir/test-files/counter.mp3
}
if { $os eq "Darwin" } {
  tclvlc media /Users/bll/Desktop/BallroomDJ.app/Contents/MacOS/test.dir/test-files/counter.mp3
}
if { $os eq "Windows" } {
  tclvlc media [file nativename {C:/Users/bll/Desktop/BallroomDJ/test.dir/test-files/counter.mp3}]
}
proc doplay  { a } { puts "play... ($a)" }
tclvlc callback playing {doplay hello}
tclvlc seek 0.0387
tclvlc play
set ::x 0
after 2000 set ::x 1
vwait ::x
tclvlc stop
tclvlc close
exit

tclvlc.c

/*
 * Copyright 2016-2017 Brad Lanam Walnut Creek CA US
 * MIT License
 */

#include <tcl.h>
#include <string.h>
#include <sys/stat.h>
#include <vlc/vlc.h>

typedef struct { char *name; Tcl_ObjCmdProc *proc; } EnsembleData;

typedef struct {
  Tcl_ObjCmdProc    *cmd;
  Tcl_Obj           *obj;
} cmdStack_t;
#define CMDSTACKMAX 5

typedef struct {
  libvlc_state_t        state;
  const char *          name;
} stateMap_t;

static const stateMap_t stateMap[] = {
  { libvlc_NothingSpecial,  "idle" },
  { libvlc_Opening,         "opening" },
  { libvlc_Buffering,       "buffering" },
  { libvlc_Playing,         "playing" },
  { libvlc_Paused,          "paused" },
  { libvlc_Stopped,         "stopped" },
  { libvlc_Ended,           "ended" },
  { libvlc_Error,           "error" }
};
#define stateMapMax (sizeof(stateMap)/sizeof(stateMap_t))

typedef struct {
  Tcl_Interp            *interp;
  libvlc_instance_t     *inst;
  const char            *version;
  libvlc_media_player_t *mp;
  libvlc_state_t        state;
  int                   argc;
  const char            **argv;
  cmdStack_t            cmdStack [CMDSTACKMAX];
  int                   stacksize;
  Tcl_Obj               *callback[stateMapMax];
} vlcData_t;

const char *
stateToStr (
  libvlc_state_t    state
  )
{
  int        i;
  const char *tptr;

  tptr = "";
  for (i = 0; i < stateMapMax; ++i) {
    if (state == stateMap[i].state) {
      tptr = stateMap[i].name;
      break;
    }
  }
  return tptr;
}

libvlc_state_t
stateToValue (
  char *name
  )
{
  int   i;
  libvlc_state_t state;

  state = libvlc_NothingSpecial;
  for (i = 0; i < stateMapMax; ++i) {
    if (strcmp (name, stateMap[i].name) == 0) {
      state = stateMap[i].state;
      break;
    }
  }
  return state;
}

void
vlcPushStack (
  vlcData_t         *vlcData,
  Tcl_ObjCmdProc    *proc,
  int               objc,
  Tcl_Obj           * const * objv
  )
{
  int       i;

  if (vlcData->stacksize >= CMDSTACKMAX) {
    return;
  }
  vlcData->cmdStack[vlcData->stacksize].cmd = proc;
  vlcData->cmdStack[vlcData->stacksize].obj = Tcl_NewListObj (objc, objv);
  ++vlcData->stacksize;
}

void
vlcExecStack (
  vlcData_t         *vlcData
  )
{
  int               i;
  int               j;
  Tcl_ObjCmdProc    *proc;
  int               objc;
  Tcl_Obj           **objv;

  for (i = 0; i < vlcData->stacksize; ++i) {
    proc = vlcData->cmdStack[i].cmd;
    Tcl_ListObjGetElements (vlcData->interp, vlcData->cmdStack[i].obj, &objc, &objv);
    (*proc) ((ClientData) vlcData, vlcData->interp, objc, objv);
  }
  vlcData->stacksize = 0;
}

void
vlcEventHandler (
  const struct libvlc_event_t *event,
  void *cd
  )
{
  vlcData_t     *vlcData = (vlcData_t *) cd;

  if (event->type == libvlc_MediaStateChanged) {
    vlcData->state = event->u.media_state_changed.new_state;
    if (vlcData->state == libvlc_Playing) {
      vlcExecStack (vlcData);
    }
    if (vlcData->callback[vlcData->state] != NULL) {
      Tcl_EvalObjEx (vlcData->interp,
          vlcData->callback[vlcData->state], 0);
    }
  }
}


int
vlcCallbackCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  libvlc_state_t        state;
  vlcData_t     *vlcData = (vlcData_t *) cd;

  if (objc < 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "state ?proc? ?args?");
    return TCL_ERROR;
  }
  state = stateToValue (Tcl_GetString(objv[1]));
  if (objc > 2) {
    vlcData->callback[state] = Tcl_NewListObj (1, objv + 2);
    Tcl_IncrRefCount (vlcData->callback[state]);
  } else if (vlcData->callback[state] != NULL) {
    Tcl_DecrRefCount (vlcData->callback[state]);
    vlcData->callback[state] = NULL;
  }

  return TCL_OK;
}

int
vlcDurationCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  libvlc_time_t     tm;
  vlcData_t *vlcData = (vlcData_t *) cd;

  if (objc != 1) {
    Tcl_WrongNumArgs(interp, 1, objv, "");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    tm = libvlc_media_player_get_length (vlcData->mp);
    Tcl_SetObjResult (interp, Tcl_NewLongObj (tm));
  }
  return rc;
}

int
vlcGetTimeCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  libvlc_time_t     tm;
  vlcData_t *vlcData = (vlcData_t *) cd;

  if (objc != 1) {
    Tcl_WrongNumArgs(interp, 1, objv, "");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    tm = libvlc_media_player_get_time (vlcData->mp);
    Tcl_SetObjResult (interp, Tcl_NewLongObj (tm));
  }
  return rc;
}

int
vlcMediaCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  libvlc_media_t    *m;
  libvlc_event_manager_t    *em;
  vlcData_t         *vlcData = (vlcData_t *) cd;
  char              *fn;
  struct stat       statinfo;

  if (objc != 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "mediapath");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    fn = Tcl_GetString(objv[1]);
    if (stat (fn, &statinfo) != 0) {
      rc = TCL_ERROR;
      return rc;
    }
    m = libvlc_media_new_path (vlcData->inst, fn);
    libvlc_media_player_set_rate (vlcData->mp, 1.0);
    em = libvlc_media_event_manager (m);
    libvlc_event_attach (em, libvlc_MediaStateChanged,
        &vlcEventHandler, vlcData);
    libvlc_media_player_set_media (vlcData->mp, m);
    libvlc_media_release (m);
  }
  return rc;
}

int
vlcPauseCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int   rc;
  vlcData_t     *vlcData = (vlcData_t *) cd;

  if (objc != 1) {
    Tcl_WrongNumArgs(interp, 1, objv, "");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    if (vlcData->state == libvlc_Opening ||
        vlcData->state == libvlc_Buffering) {
      ;
    } else if (vlcData->state == libvlc_Playing) {
      libvlc_media_player_set_pause (vlcData->mp, 1);
    } else if (vlcData->state == libvlc_Paused) {
      libvlc_media_player_set_pause (vlcData->mp, 0);
    }
  }
  return rc;
}

int
vlcPlayCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int   rc;
  vlcData_t     *vlcData = (vlcData_t *) cd;

  if (objc != 1) {
    Tcl_WrongNumArgs(interp, 1, objv, "");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    libvlc_media_player_play (vlcData->mp);
  }
  return rc;
}

int
vlcRateCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  vlcData_t *vlcData = (vlcData_t *) cd;
  float     rate;
  double    d;


  if (objc != 1 && objc != 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "?rate?");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    if (objc == 2) {
      if (vlcData->state == libvlc_Playing) {
        rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
        if (rc == TCL_OK) {
          rate = (float) d;
          libvlc_media_player_set_rate (vlcData->mp, rate);
        }
      } else {
        vlcPushStack (vlcData, &vlcRateCmd, objc, objv);
      }
    }

    rate = libvlc_media_player_get_rate (vlcData->mp);
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) rate));
  }
  return rc;
}

int
vlcSeekCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  vlcData_t *vlcData = (vlcData_t *) cd;
  float     pos;
  double    d;


  if (objc != 1 && objc != 2) {
    Tcl_WrongNumArgs(interp, 1, objv, "?position?");
    return TCL_ERROR;
  }

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    if (objc == 2) {
      if (vlcData->state == libvlc_Playing) {
        rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
        if (rc == TCL_OK) {
          pos = (float) d;
          libvlc_media_player_set_position (vlcData->mp, pos);
        }
      } else {
        vlcPushStack (vlcData, &vlcSeekCmd, objc, objv);
      }
    }
    pos = libvlc_media_player_get_position (vlcData->mp);
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) pos));
  }
  return rc;
}

int
vlcStateCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  libvlc_state_t    plstate;
  vlcData_t         *vlcData = (vlcData_t *) cd;

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    plstate = vlcData->state;
    Tcl_SetObjResult (interp, Tcl_NewStringObj (stateToStr(plstate), -1));
  }
  return rc;
}

int
vlcStopCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int   rc;
  vlcData_t     *vlcData = (vlcData_t *) cd;

  rc = TCL_OK;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    libvlc_media_player_stop (vlcData->mp);
  }
  return rc;
}

void
vlcClose (
  vlcData_t     *vlcData
  )
{
  int   i;

  for (i = 0; i < stateMapMax; ++i) {
    if (vlcData->callback[i] != NULL) {
      Tcl_DecrRefCount (vlcData->callback[i]);
    }
    vlcData->callback[i] = NULL;
  }
  if (vlcData->mp != NULL) {
    libvlc_media_player_stop (vlcData->mp);
    libvlc_media_player_release (vlcData->mp);
    vlcData->mp = NULL;
  }
  if (vlcData->inst != NULL) {
    libvlc_release (vlcData->inst);
    vlcData->inst = NULL;
  }
  if (vlcData->argv != NULL) {
    for (i = 0; i < vlcData->argc; ++i) {
      ckfree (vlcData->argv[i]);
    }
    ckfree (vlcData->argv);
    vlcData->argv = NULL;
  }

  vlcData->state = libvlc_NothingSpecial;
  vlcData->stacksize = 0;
}

void
vlcExitHandler (
  void *cd
  )
{
  vlcData_t     *vlcData = (vlcData_t *) cd;

  vlcClose (vlcData);
  ckfree (cd);
}

int
vlcReleaseCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  vlcData_t     *vlcData = (vlcData_t *) cd;

  vlcClose (vlcData);
  return TCL_OK;
}

int
vlcInitCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  char          *tptr;
  char          *nptr;
  int           rc;
  int           i;
  int           len;
  vlcData_t     *vlcData = (vlcData_t *) cd;

  vlcData->argv = (const char **) ckalloc (sizeof(const char *) * (size_t) (objc + 1));
  for (i = 0; i < objc; ++i) {
    tptr = Tcl_GetStringFromObj (objv[i], &len);
    nptr = (char *) ckalloc (len+1);
    strcpy (nptr, tptr);
    vlcData->argv[i] = nptr;
  }
  vlcData->argc = objc;
  vlcData->argv[objc] = NULL;

  rc = TCL_ERROR;
  vlcData->version = libvlc_get_version ();
  if (vlcData->inst == NULL) {
    vlcData->inst = libvlc_new (objc, vlcData->argv);
  }
  if (vlcData->inst != NULL && vlcData->mp == NULL) {
    vlcData->mp = libvlc_media_player_new (vlcData->inst);
  }
  if (vlcData->inst != NULL && vlcData->mp != NULL) {
    rc = TCL_OK;
    libvlc_audio_set_volume (vlcData->mp, 100);
    Tcl_CreateExitHandler (vlcExitHandler, cd);
  }

  return rc;
}

static const EnsembleData vlcCmdMap[] = {
  { "callback",   vlcCallbackCmd },
  { "close",      vlcReleaseCmd },
  { "duration",   vlcDurationCmd },
  { "gettime",    vlcGetTimeCmd },
  { "init",       vlcInitCmd },
  { "media",      vlcMediaCmd },
  { "pause",      vlcPauseCmd },
  { "play",       vlcPlayCmd },
  { "rate",       vlcRateCmd },
  { "seek",       vlcSeekCmd },
  { "state",      vlcStateCmd },
  { "stop",       vlcStopCmd },
  { NULL, NULL }
};

int
Tclvlc_Init (Tcl_Interp *interp)
{
  Tcl_Namespace *nsPtr = NULL;
  Tcl_Command   ensemble = NULL;
  Tcl_Obj       *dictObj = NULL;
  Tcl_DString   ds;
  vlcData_t     *vlcData;
  int           i;
  const char    *nsName = "::tcl::tclvlc";
  const char    *cmdName = nsName + 5;

#ifdef USE_TCL_STUBS
  if (!Tcl_InitStubs (interp,"8.3",0)) {
    return TCL_ERROR;
  }
#else
  if (!Tcl_PkgRequire (interp,"Tcl","8.3",0)) {
    return TCL_ERROR;
  }
#endif

  vlcData = (vlcData_t *) ckalloc (sizeof (vlcData_t));
  vlcData->interp = interp;
  vlcData->inst = NULL;
  vlcData->mp = NULL;
  vlcData->argv = NULL;
  vlcData->state = libvlc_NothingSpecial;
  vlcData->stacksize = 0;
  for (i = 0; i < stateMapMax; ++i) {
    vlcData->callback[i] = NULL;
  }

  nsPtr = Tcl_CreateNamespace(interp, nsName, NULL, 0);
  if (nsPtr == NULL) {
    Tcl_Panic ("failed to create namespace: %s\n", nsName);
  }
  ensemble = Tcl_CreateEnsemble(interp, cmdName, nsPtr, TCL_ENSEMBLE_PREFIX);
  if (ensemble == NULL) {
    Tcl_Panic ("failed to create ensemble: %s\n", cmdName);
  }
  Tcl_DStringInit (&ds);
  Tcl_DStringAppend (&ds, nsName, -1);

  dictObj = Tcl_NewObj();
  for (i = 0; vlcCmdMap[i].name != NULL; ++i) {
    Tcl_Obj *nameObj;
    Tcl_Obj *fqdnObj;

    nameObj = Tcl_NewStringObj (vlcCmdMap[i].name, -1);
    fqdnObj = Tcl_NewStringObj (Tcl_DStringValue(&ds), Tcl_DStringLength(&ds));
    Tcl_AppendStringsToObj (fqdnObj, "::", vlcCmdMap[i].name, NULL);
    Tcl_DictObjPut (NULL, dictObj, nameObj, fqdnObj);
    if (vlcCmdMap[i].proc) {
      Tcl_CreateObjCommand (interp, Tcl_GetString (fqdnObj),
           vlcCmdMap[i].proc, (ClientData) vlcData, NULL);
    }
  }

  if (ensemble) {
    Tcl_SetEnsembleMappingDict (interp, ensemble, dictObj);
  }

  Tcl_DStringFree(&ds);

  Tcl_PkgProvide (interp, cmdName+2, "0.1");
  return TCL_OK;
}

mkvlc.sh

#!/bin/sh

tv=8.6
slext=.so
os=`uname -s`
arch=`uname -m`
bits=64
case $arch in
  i?86*)
    bits=32
    ;;
esac
inc=-I/usr/include/tcl${tv}
lib=
lv=$tv
if [ $os = "Darwin" ]; then
  slext=.dylib
  # for MacPorts tcl
  inc=-I/opt/local/include
  lib=-L/opt/local/lib
  inc+=" -I/Applications/VLC.app/Contents/MacOS/include"
  lib+=" -L/Applications/VLC.app/Contents/MacOS/lib"
fi

${CC:-cc} -O -shared -fPIC -o tclvlc${slext} $inc -DUSE_TCL_STUBS tclvlc.c $lib -ltclstub${lv} -lvlc
test -d $os || mkdir $os
if [ -f tclvlc${slext} ]; then
  echo "tclvlc success"
  mv -f tclvlc${slext} $os/tclvlc${bits}${slext}
fi
if [ $os = "Darwin" ]; then
  install_name_tool -change "@loader_path/lib/libvlc.5.dylib" "/Applications/VLC.app/Contents/MacOS/lib/libvlc.5.dylib" Darwin/*
fi

winmkvlc.sh

#!/bin/bash

test -d Windows || mkdir Windows

case $MSYSTEM in
  *32)
    gcc -m32 -shared -static-libgcc -DWIN_TCL_INTERFACE -o Windows/tclvlc32.dll \
        -I'/home/bll/vlc/vlc-2.2.4/include' \
        '-Wl,-rpath=/c/Program Files/VideoLAN/vlc' \
        -I$HOME/local-32/include -DUSE_TCL_STUBS tclvlc.c \
        -L$HOME/local-32/lib -ltclstub86 \
        -L'/c/Program Files/VideoLAN/vlc' -lvlc
    ;;
  *64)
    gcc -m64 -shared -static-libgcc -DWIN_TCL_INTERFACE -o Windows/tclvlc64.dll \
        -I'/home/bll/vlc/vlc-2.2.4/include' \
        '-Wl,-rpath=/c/Program Files/VideoLAN/vlc' \
        -I$HOME/local-64/include -DUSE_TCL_STUBS tclvlc.c \
        -L$HOME/local-64/lib -ltclstub86 \
        -L'/c/Program Files/VideoLAN/vlc' -lvlc
    ;;
esac