Version 5 of VLC Tcl Extension

Updated 2016-12-30 01:17:03 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.

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.

I have given up trying to get callbacks working. Calling Tcl code from the VLC event handler does not seem to work properly on Windows.

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-media-library --no-playlist-autostart \
    --no-loop --no-random --no-repeat --quiet --play-and-stop \
    --audio-filter scaletempo

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}]
}

tclvlc play
set state [tclvlc state]
while { $state eq "opening" || $state eq "buffering" } {
  after 100
  set state [tclvlc state]
}
tclvlc seek 0.0387
set ::x 0
after 1000 set ::x 1
vwait ::x
tclvlc close
exit

tclvlc.c

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

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

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

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;
  char                  version [40];
  libvlc_media_player_t *mp;
  libvlc_state_t        state;
  int                   argc;
  const char            **argv;
} 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
vlcEventHandler (
  const struct libvlc_event_t *event,
  void *cd
  )
{
  vlcData_t     *vlcData = (vlcData_t *) cd;
  int           objc;
  Tcl_Obj       **objv;
  Tcl_Obj       *tobj;

  if (event->type == libvlc_MediaStateChanged) {
    vlcData->state = event->u.media_state_changed.new_state;
  }
}

int
vlcDurationCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  libvlc_time_t     tm;
  double            dtm;
  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);
    dtm = (double) tm / 1000.0;
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj (dtm));
  }
  return rc;
}

int
vlcGetTimeCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  libvlc_time_t     tm;
  double            dtm;
  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);
    dtm = (double) tm / 1000.0;
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj (dtm));
  }
  return rc;
}

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

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

  rc = TCL_OK;
  rval = 0;
  if (vlcData->inst == NULL || vlcData->mp == NULL) {
    rc = TCL_ERROR;
  } else {
    if (vlcData->state == libvlc_Opening ||
        vlcData->state == libvlc_Buffering ||
        vlcData->state == libvlc_Playing) {
      rval = 1;
    }
    Tcl_SetObjResult (interp, Tcl_NewIntObj (rval));
  }
  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 && 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);
      }
    }

    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;
  libvlc_time_t     tm;
  double            dtm;
  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 && vlcData->state == libvlc_Playing) {
      tm = libvlc_media_player_get_length (vlcData->mp);
      dtm = (double) tm / 1000.0;
      rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
      d = d / dtm;
      if (rc == TCL_OK) {
        pos = (float) d;
        libvlc_media_player_set_position (vlcData->mp, pos);
      }
    }
    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;
}

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

  Tcl_SetObjResult (interp, Tcl_NewStringObj (vlcData->version, -1));
  return TCL_OK;
}

void
vlcClose (
  vlcData_t     *vlcData
  )
{
  int   i;

  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;
}

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;

  strcpy (vlcData->version, libvlc_get_version ());
  tptr = strchr (vlcData->version, ' ');
  if (nptr != NULL) {
    *tptr = '\0';
  }

  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[] = {
  { "close",      vlcReleaseCmd },
  { "duration",   vlcDurationCmd },
  { "gettime",    vlcGetTimeCmd },
  { "init",       vlcInitCmd },
  { "isplay",     vlcIsPlayCmd },
  { "media",      vlcMediaCmd },
  { "pause",      vlcPauseCmd },
  { "play",       vlcPlayCmd },
  { "rate",       vlcRateCmd },
  { "seek",       vlcSeekCmd },
  { "state",      vlcStateCmd },
  { "stop",       vlcStopCmd },
  { "version",    vlcVersionCmd },
  { 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;
  int           rc;
  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;

  nsPtr = Tcl_FindNamespace(interp, nsName, NULL, 0);
  if (nsPtr == 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 (x86)/VideoLAN/vlc' \
        -I$HOME/local-32/include -DUSE_TCL_STUBS tclvlc.c \
        -L$HOME/local-32/lib -ltclstub86 \
        -L'/c/Program Files (x86)/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