Updated 2018-04-17 00:09:59 by bll

MPV Tcl Extension edit

bll 2018-3-1 This is a Tcl interface to the MPV audio player, using libmpv. Linux users should be able to readily install libmpv. Windows users can get libmpv from: https://sourceforge.net/projects/mpv-player-windows/files/libmpv/ . I have not found a pre-compiled libmpv for Mac OS.

Like the VLC Tcl Extension, I only need audio, and don't use playlists.

This code has not been extensively tested, I don't yet know how robust it is. At this time, it has only been tested on Linux and Windows 7 (64-bit).

 Change Log

bll 2018-4-16 Internal state on seek is still playing. Update state table. Reinitialize duration and time on media load.

bll 2018-3-30 Update: change provided version number to 1.1 rather than using the MPV version number.

See Also: VLC Tcl Extension

tclmpv.c
/*
 * Copyright 2018 Brad Lanam Walnut Creek CA US
 */

#define MPVDEBUG 0
#define USE_TCL_STUBS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <tcl.h>
#include <mpv/client.h>

#define CHKTIMER 10

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

typedef enum playstate {
  PS_NONE = 0,
  PS_IDLE = 1,
  PS_OPENING = 2,
    /* for MPV, mark the state as buffering until we receive the */
    /* duration property change */
  PS_BUFFERING = 3,
  PS_PLAYING = 4,
  PS_PAUSED = 5,
  PS_STOPPED = 6,
  PS_ERROR = 7
} playstate;

typedef struct {
  playstate             state;
  const char *          name;
} playStateMap_t;

static const playStateMap_t playStateMap[] = {
  { PS_NONE, "none" },
  { PS_IDLE, "idle" },
  { PS_OPENING, "opening" },
  { PS_BUFFERING, "buffering" },
  { PS_PLAYING, "playing" },
  { PS_PAUSED, "paused" },
  { PS_STOPPED, "stopped" },
  { PS_ERROR, "error" }
};
#define playStateMapMax (sizeof(playStateMap)/sizeof(playStateMap_t))

typedef struct {
  mpv_event_id          state;
  const char *          name;
  playstate             stateflag;
} stateMap_t;

static const stateMap_t stateMap[] = {
  { MPV_EVENT_NONE,             "none", PS_NONE },
  { MPV_EVENT_IDLE,             "idle", PS_IDLE },
  { MPV_EVENT_START_FILE,       "opening", PS_OPENING },
  { MPV_EVENT_FILE_LOADED,      "playing", PS_BUFFERING },
  { MPV_EVENT_PROPERTY_CHANGE,  "property-chg", PS_NONE },
  { MPV_EVENT_SEEK,             "seeking", PS_NONE },
  { MPV_EVENT_PLAYBACK_RESTART, "playafterseek", PS_NONE },
  { MPV_EVENT_END_FILE,         "stopped", PS_STOPPED },
  { MPV_EVENT_SHUTDOWN,         "ended", PS_STOPPED },
    /* these next three are only for debugging */
  { MPV_EVENT_TRACKS_CHANGED,   "tracks-changed", PS_NONE },
  { MPV_EVENT_AUDIO_RECONFIG,   "audio-reconf", PS_NONE },
  { MPV_EVENT_METADATA_UPDATE,  "metadata-upd", PS_NONE }
};
#define stateMapMax (sizeof(stateMap)/sizeof(stateMap_t))

#define stateMapIdxMax 40 /* mpv currently has 24 states coded */
typedef struct {
  Tcl_Interp            *interp;
  mpv_handle            *inst;
  char                  version [40];
  mpv_event_id          state;
  int                   argc;
  const char            **argv;
  const char            *device;
  double                duration;
  double                tm;
  int                   paused;
  int                   hasEvent;       /* flag to process mpv event */
  Tcl_TimerToken        timerToken;
  int                   stateMapIdx [stateMapIdxMax];
  FILE                  *debugfh;
} mpvData_t;

const char *
mpvStateToStr (
  mpv_event_id      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;
}

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

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

/* executed in some arbitrary thread */
void
mpvCallbackHandler (
  void *cd
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;

  mpvData->hasEvent = 1;
}

void
mpvEventHandler (
  ClientData cd
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;
  playstate     stateflag;

  if (mpvData->inst == NULL) {
    return;
  }
  if (mpvData->hasEvent == 0) {
    mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData);
    return;
  }

  mpvData->hasEvent = 0;

  mpv_event *event = mpv_wait_event (mpvData->inst, 0.0);
  stateflag = stateMap[(int) mpvData->stateMapIdx[event->event_id]].stateflag;

  while (event->event_id != MPV_EVENT_NONE) {
#if MPVDEBUG
    if (mpvData->debugfh != NULL) {
      fprintf (mpvData->debugfh, "mpv: ev: %d %s\n", event->event_id, mpvStateToStr (event->event_id));
    }
#endif
    if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) {
      mpv_event_property *prop = (mpv_event_property *) event->data;
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "mpv: ev: prop: %s\n", prop->name);
      }
#endif
      if (strcmp (prop->name, "time-pos") == 0) {
        if (prop->format == MPV_FORMAT_DOUBLE) {
          mpvData->tm = * (double *) prop->data;
        }
#if MPVDEBUG
        if (mpvData->debugfh != NULL) {
          fprintf (mpvData->debugfh, "mpv: ev: tm: %.2f\n", mpvData->tm);
        }
#endif
      } else if (strcmp (prop->name, "duration") == 0) {
        if (mpvData->state == PS_BUFFERING) {
          mpvData->state = PS_PLAYING;
#if MPVDEBUG
          if (mpvData->debugfh != NULL) {
            fprintf (mpvData->debugfh, "mpv: state: %s\n", stateToStr(mpvData->state));
          }
#endif
        }
        if (prop->format == MPV_FORMAT_DOUBLE) {
          mpvData->duration = * (double *) prop->data;
        }
#if MPVDEBUG
        if (mpvData->debugfh != NULL) {
          fprintf (mpvData->debugfh, "mpv: ev: dur: %.2f\n", mpvData->duration);
        }
#endif
      }
    } else if (stateflag != PS_NONE) {
      mpvData->state = stateflag;
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "mpv: state: %s\n", stateToStr(mpvData->state));
      }
#endif
    }


    mpv_event *event = mpv_wait_event (mpvData->inst, 0.0);
    stateflag = stateMap[(int) mpvData->stateMapIdx[event->event_id]].stateflag;
  }

  mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData);
}

int
mpvDurationCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  double            tm;
  double            dtm;
  mpvData_t *mpvData = (mpvData_t *) cd;

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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    tm = mpvData->duration;
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj (tm));
  }
  return rc;
}

int
mpvGetTimeCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  double    tm;
  mpvData_t *mpvData = (mpvData_t *) cd;

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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    tm = mpvData->tm;
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj (tm));
  }
  return rc;
}

int
mpvIsPlayCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       rval;
  mpvData_t *mpvData = (mpvData_t *) cd;

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

  rc = TCL_OK;
  rval = 0;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    /*
     * In order to match the implementation of VLC's internal
     * isplaying command, return true if the player is paused
     * If the telnet VLC interface is ever dropped, this interface
     * could be enhanced.
     */
    if (mpvData->state == PS_OPENING ||
        mpvData->state == PS_PLAYING ||
        mpvData->state == PS_PAUSED) {
      rval = 1;
    }
    Tcl_SetObjResult (interp, Tcl_NewIntObj (rval));
  }
  return rc;
}

int
mpvMediaCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  int               status;
  mpvData_t         *mpvData = (mpvData_t *) cd;
  char              *fn;
  struct stat       statinfo;
  double            dval;

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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    if (mpvData->device != NULL) {
      status = mpv_set_property (mpvData->inst, "audio-device", MPV_FORMAT_STRING, (void *) mpvData->device);
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "set-ad:status:%d %s\n", status, mpv_error_string(status));
      }
#endif
    }

    fn = Tcl_GetString(objv[1]);
    if (stat (fn, &statinfo) != 0) {
      rc = TCL_ERROR;
      return rc;
    }

      /* reset the duration and time */
    mpvData->duration = 0.0;
    mpvData->tm = 0.0;
    /* like many players, mpv will start playing when the 'loadfile'
     * command is executed.
     */
    const char *cmd[] = {"loadfile", fn, "replace", NULL};
    status = mpv_command (mpvData->inst, cmd);
#if MPVDEBUG
    if (mpvData->debugfh != NULL) {
      fprintf (mpvData->debugfh, "loadfile:status:%d %s\n", status, mpv_error_string(status));
    }
#endif
    dval = 1.0;
    status = mpv_set_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &dval);
#if MPVDEBUG
    if (mpvData->debugfh != NULL) {
      fprintf (mpvData->debugfh, "speed-1:status:%d %s\n", status, mpv_error_string(status));
    }
#endif
  }
  return rc;
}

int
mpvPauseCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       status;
  mpvData_t *mpvData = (mpvData_t *) cd;

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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    if (mpvData->state != PS_PLAYING && mpvData->state != PS_PAUSED) {
      ;
    } else if (mpvData->state == PS_PLAYING &&
        mpvData->paused == 0) {
      int val = 1;
      status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val);
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "pause-%d:status:%d %s\n", val, status, mpv_error_string(status));
      }
#endif
      mpvData->paused = 1;
      mpvData->state = PS_PAUSED;
    } else if (mpvData->state == PS_PAUSED &&
        mpvData->paused == 1) {
      int val = 0;
      status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val);
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "pause-%d:status:%d %s\n", val, status, mpv_error_string(status));
      }
#endif
      mpvData->paused = 0;
      mpvData->state = PS_PLAYING;
    }
  }
  return rc;
}

int
mpvPlayCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       status;
  mpvData_t *mpvData = (mpvData_t *) cd;

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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    if (mpvData->state == PS_PAUSED &&
        mpvData->paused == 1) {
      int val = 0;
      status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val);
#if MPVDEBUG
      if (mpvData->debugfh != NULL) {
        fprintf (mpvData->debugfh, "play:status:%d %s\n", status, mpv_error_string(status));
      }
#endif
      mpvData->paused = 0;
      mpvData->state = PS_PLAYING;
    }
  }
  return rc;
}

int
mpvRateCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       status;
  mpvData_t *mpvData = (mpvData_t *) cd;
  double    rate;
  double    d;


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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    if (objc == 2 && mpvData->state == PS_PLAYING) {
      rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
      if (rc == TCL_OK) {
        rate = d;
        status = mpv_set_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &rate);
#if MPVDEBUG
        if (mpvData->debugfh != NULL) {
          fprintf (mpvData->debugfh, "speed-%.2f:status:%d %s\n", rate, status, mpv_error_string(status));
        }
#endif
      }
    }

    status = mpv_get_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &rate);
#if MPVDEBUG
    if (mpvData->debugfh != NULL) {
      fprintf (mpvData->debugfh, "speed-get:status:%d %s\n", status, mpv_error_string(status));
    }
#endif
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) rate));
  }
  return rc;
}

int
mpvSeekCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       status;
  double    tm;
  double    dtm;
  mpvData_t *mpvData = (mpvData_t *) cd;
  double    pos;
  double    d;
  char      spos [40];


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

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    if (objc == 2 && mpvData->state == PS_PLAYING) {
      rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
      if (rc == TCL_OK) {
        pos = (double) d;
        sprintf (spos, "%.1f", pos);
        const char *cmd[] = { "seek", spos, "absolute", NULL };
        status = mpv_command (mpvData->inst, cmd);
#if MPVDEBUG
        if (mpvData->debugfh != NULL) {
          fprintf (mpvData->debugfh, "seek-%s:status:%d %s\n", spos, status, mpv_error_string(status));
        }
#endif
      }
    }
    tm = mpvData->tm;
    Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) tm));
  }
  return rc;
}

int
mpvStateCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int               rc;
  mpv_event_id      plstate;
  mpvData_t         *mpvData = (mpvData_t *) cd;

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

int
mpvStopCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;
  int       status;
  mpvData_t *mpvData = (mpvData_t *) cd;

  rc = TCL_OK;
  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    /* stop: stops playback and clears playlist */
    /* difference: vlc's stop command does not clear the playlist */
    const char *cmd[] = {"stop", NULL};
    status = mpv_command (mpvData->inst, cmd);
#if MPVDEBUG
    if (mpvData->debugfh != NULL) {
      fprintf (mpvData->debugfh, "stop:status:%d %s\n", status, mpv_error_string(status));
    }
#endif
  }
  return rc;
}

int
mpvHaveAudioDevListCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  int       rc;

  rc = 1;
  Tcl_SetObjResult (interp, Tcl_NewBooleanObj (rc));
  return TCL_OK;
}

int
mpvVersionCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;

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

void
mpvClose (
  mpvData_t     *mpvData
  )
{
  int   i;

  if (mpvData->inst != NULL) {
    mpv_terminate_destroy (mpvData->inst);
    mpvData->inst = NULL;
  }
  if (mpvData->argv != NULL) {
    for (i = 0; i < mpvData->argc; ++i) {
      ckfree (mpvData->argv[i]);
    }
    ckfree (mpvData->argv);
    mpvData->argv = NULL;
  }

  if (mpvData->device != NULL) {
    free ((void *) mpvData->device);
    mpvData->device = NULL;
  }

  mpvData->state = PS_STOPPED;
}

void
mpvExitHandler (
  void *cd
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;


  Tcl_DeleteTimerHandler (mpvData->timerToken);
  mpvClose (mpvData);
  if (mpvData->debugfh != NULL) {
    fclose (mpvData->debugfh);
    mpvData->debugfh = NULL;
  }
  ckfree (cd);
}

int
mpvReleaseCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;

  Tcl_DeleteTimerHandler (mpvData->timerToken);
  mpvClose (mpvData);
  if (mpvData->debugfh != NULL) {
    fclose (mpvData->debugfh);
    mpvData->debugfh = NULL;
  }
  return TCL_OK;
}

int
mpvInitCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  char          *tptr;
  char          *nptr;
  int           rc;
  int           i;
  int           len;
  int           status;
  int           gstatus;
  mpvData_t     *mpvData = (mpvData_t *) cd;

  mpvData->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);
    mpvData->argv[i] = nptr;
  }
  mpvData->argc = objc;
  mpvData->argv[objc] = NULL;

  rc = TCL_ERROR;
  gstatus = 0;

  if (mpvData->inst == NULL) {
    mpvData->inst = mpv_create ();
    if (mpvData->inst != NULL) {
      status = mpv_initialize (mpvData->inst);
      if (status < 0) { gstatus = status; }
      double vol = 100.0;
      mpv_set_property (mpvData->inst, "volume", MPV_FORMAT_DOUBLE, &vol);
      mpv_observe_property(mpvData->inst, 0, "duration", MPV_FORMAT_DOUBLE);
      mpv_observe_property(mpvData->inst, 0, "time-pos", MPV_FORMAT_DOUBLE);
      mpv_set_wakeup_callback (mpvData->inst, &mpvCallbackHandler, mpvData);
    }
  }
  if (mpvData->inst != NULL && gstatus == 0) {
    rc = TCL_OK;
  }
  return rc;
}

int
mpvAudioDevSetCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;
  int           rc;
  char          *dev;

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

  rc = TCL_OK;

  if (mpvData->inst == NULL) {
    rc = TCL_ERROR;
  } else {
    dev = Tcl_GetString(objv[1]);
    if (mpvData->device != NULL) {
      free ((void *) mpvData->device);
    }
    mpvData->device = NULL;
    if (strlen (dev) > 0) {
      mpvData->device = strdup (dev);
    }
  }

  return rc;
}

int
mpvAudioDevListCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  mpvData_t     *mpvData = (mpvData_t *) cd;
  Tcl_Obj       *lobj;
  Tcl_Obj       *sobj;
  mpv_node      anodes;
  mpv_node_list *infolist;
  mpv_node_list *nodelist;
  char          *nmptr;
  char          *descptr;
  int           status;

  if (mpvData->inst == NULL) {
    return TCL_ERROR;
  }

  lobj = Tcl_NewListObj (0, NULL);

  status = mpv_get_property (mpvData->inst, "audio-device-list", MPV_FORMAT_NODE, &anodes);
  if (anodes.format != MPV_FORMAT_NODE_ARRAY) {
    return TCL_ERROR;
  }
  nodelist = anodes.u.list;
  for (int i = 0; i < nodelist->num; ++i) {
    infolist = nodelist->values[i].u.list;
    for (int j = 0; j < infolist->num; ++j) {
      if (strcmp (infolist->keys[j], "name") == 0) {
        nmptr = infolist->values[j].u.string;
      } else if (strcmp (infolist->keys[j], "description") == 0) {
        descptr = infolist->values[j].u.string;
      } else {
        if (mpvData->debugfh != NULL) {
          fprintf (mpvData->debugfh, "dev: %s\n", infolist->keys[j]);
        }
      }
    }
    sobj = Tcl_NewStringObj (nmptr, -1);
    Tcl_ListObjAppendElement (interp, lobj, sobj);
    sobj = Tcl_NewStringObj (descptr, -1);
    Tcl_ListObjAppendElement (interp, lobj, sobj);
  }
  mpv_free_node_contents (&anodes);
  Tcl_SetObjResult (interp, lobj);
  return TCL_OK;
}

static const EnsembleData mpvCmdMap[] = {
  { "audiodevlist", mpvAudioDevListCmd },
  { "audiodevset",  mpvAudioDevSetCmd },
  { "close",        mpvReleaseCmd },
  { "duration",     mpvDurationCmd },
  { "gettime",      mpvGetTimeCmd },
  { "init",         mpvInitCmd },
  { "haveaudiodevlist", mpvHaveAudioDevListCmd },
  { "isplay",       mpvIsPlayCmd },
  { "media",        mpvMediaCmd },
  { "pause",        mpvPauseCmd },
  { "play",         mpvPlayCmd },
  { "rate",         mpvRateCmd },
  { "seek",         mpvSeekCmd },
  { "state",        mpvStateCmd },
  { "stop",         mpvStopCmd },
  { "version",      mpvVersionCmd },
  { NULL, NULL }
};

int
Tclmpv_Init (Tcl_Interp *interp)
{
  Tcl_Namespace *nsPtr = NULL;
  Tcl_Command   ensemble = NULL;
  Tcl_Obj       *dictObj = NULL;
  Tcl_DString   ds;
  mpvData_t     *mpvData;
  unsigned int  ivers;
  int           i;
  int           rc;
  int           debug;
  const char    *nsName = "::tcl::tclmpv";
  const char    *cmdName = nsName + 5;
  char          tvers [20];

  if (!Tcl_InitStubs (interp,"8.3",0)) {
    return TCL_ERROR;
  }

  debug = 0;
#if MPVDEBUG
  debug = 1;
#endif
  mpvData = (mpvData_t *) ckalloc (sizeof (mpvData_t));
  mpvData->interp = interp;
  mpvData->inst = NULL;
  mpvData->argv = NULL;
  mpvData->state = PS_NONE;
  mpvData->device = NULL;
  mpvData->paused = 0;
  mpvData->duration = 0.0;
  mpvData->tm = 0.0;
  mpvData->hasEvent = 0;
  mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData);
  mpvData->debugfh = NULL;
  for (i = 0; i < stateMapIdxMax; ++i) {
    mpvData->stateMapIdx[i] = 0;
  }
  for (i = 0; i < stateMapMax; ++i) {
    mpvData->stateMapIdx[stateMap[i].state] = i;
  }

  if (debug) {
    mpvData->debugfh = fopen ("mpvdebug.txt", "w+");
  }

  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; mpvCmdMap[i].name != NULL; ++i) {
    Tcl_Obj *nameObj;
    Tcl_Obj *fqdnObj;

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

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

  Tcl_DStringFree(&ds);

  ivers = mpv_client_api_version();
  sprintf (tvers, "%d.%d", ivers >> 16, ivers & 0xFF);
  strcpy (mpvData->version, tvers);

  /* If the 'package ifneeded' and package provides do
   * not match, tcl fails.  Can't really use the mpv
   * version number here.
   */
  Tcl_PkgProvide (interp, cmdName+2, "1.1");

  return TCL_OK;
}