/*
 * Copyright (C) 2010 Ole André Vadla Ravnås <oravnas@cisco.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "celvideosrc.h"

#include "coremediabuffer.h"

#include <gst/video/video.h>

#define DEFAULT_DEVICE_INDEX  -1
#define DEFAULT_DO_STATS      FALSE

#define QUEUE_READY_LOCK(instance) GST_OBJECT_LOCK (instance)
#define QUEUE_READY_UNLOCK(instance) GST_OBJECT_UNLOCK (instance)
#define QUEUE_READY_WAIT(instance) \
    g_cond_wait (instance->ready_cond, GST_OBJECT_GET_LOCK (instance))
#define QUEUE_READY_NOTIFY(instance) g_cond_signal (instance->ready_cond)

GST_DEBUG_CATEGORY (gst_cel_video_src_debug);
#define GST_CAT_DEFAULT gst_cel_video_src_debug

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_YUV ("NV12") ";"
        GST_VIDEO_CAPS_YUV ("YUY2"))
    );

enum
{
  PROP_0,
  PROP_DEVICE_INDEX,
  PROP_DO_STATS,
  PROP_FPS
};

typedef struct
{
  guint index;

  GstVideoFormat video_format;
  guint32 fourcc;
  gint width;
  gint height;
  gint fps_n;
  gint fps_d;
} GstCelVideoFormat;

static gboolean gst_cel_video_src_open_device (GstCelVideoSrc * self);
static void gst_cel_video_src_close_device (GstCelVideoSrc * self);
static void gst_cel_video_src_ensure_device_caps_and_formats
    (GstCelVideoSrc * self);
static void gst_cel_video_src_release_device_caps_and_formats
    (GstCelVideoSrc * self);
static gboolean gst_cel_video_src_select_format (GstCelVideoSrc * self,
    GstCelVideoFormat * format);

static gboolean gst_cel_video_src_parse_stream_format
    (GstCelVideoSrc * self, guint index, CFDictionaryRef stream_format,
    GstCelVideoFormat * format);
static OSStatus gst_cel_video_src_set_stream_property_i32
    (GstCelVideoSrc * self, CFStringRef name, SInt32 value);
static OSStatus gst_cel_video_src_set_stream_property_value
    (GstCelVideoSrc * self, CFStringRef name, CFTypeRef value);

static GstPushSrcClass *parent_class;

GST_BOILERPLATE (GstCelVideoSrc, gst_cel_video_src, GstPushSrc,
    GST_TYPE_PUSH_SRC);

static void
gst_cel_video_src_init (GstCelVideoSrc * self, GstCelVideoSrcClass * gclass)
{
  GstBaseSrc *base_src = GST_BASE_SRC_CAST (self);

  gst_base_src_set_live (base_src, TRUE);
  gst_base_src_set_format (base_src, GST_FORMAT_TIME);

  self->ready_cond = g_cond_new ();
}

static void
gst_cel_video_src_dispose (GObject * object)
{
  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
gst_cel_video_src_finalize (GObject * object)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (object);

  g_cond_free (self->ready_cond);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_cel_video_src_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (object);

  switch (prop_id) {
    case PROP_DEVICE_INDEX:
      g_value_set_int (value, self->device_index);
      break;
    case PROP_DO_STATS:
      g_value_set_boolean (value, self->do_stats);
      break;
    case PROP_FPS:
      GST_OBJECT_LOCK (object);
      g_value_set_int (value, self->fps);
      GST_OBJECT_UNLOCK (object);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_cel_video_src_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (object);

  switch (prop_id) {
    case PROP_DEVICE_INDEX:
      self->device_index = g_value_get_int (value);
      break;
    case PROP_DO_STATS:
      self->do_stats = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static GstStateChangeReturn
gst_cel_video_src_change_state (GstElement * element, GstStateChange transition)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (element);
  GstStateChangeReturn ret;

  switch (transition) {
    case GST_STATE_CHANGE_NULL_TO_READY:
      if (!gst_cel_video_src_open_device (self))
        goto open_failed;
      break;
    default:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_READY_TO_NULL:
      gst_cel_video_src_close_device (self);
      break;
    default:
      break;
  }

  return ret;

  /* ERRORS */
open_failed:
  {
    return GST_STATE_CHANGE_FAILURE;
  }
}

static GstCaps *
gst_cel_video_src_get_caps (GstBaseSrc * basesrc)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (basesrc);
  GstCaps *result;

  if (self->device != NULL) {
    gst_cel_video_src_ensure_device_caps_and_formats (self);

    result = gst_caps_ref (self->device_caps);
  } else {
    result = NULL;
  }

  if (result != NULL) {
    gchar *str;

    str = gst_caps_to_string (result);
    GST_DEBUG_OBJECT (self, "returning: %s", str);
    g_free (str);
  }

  return result;
}

static gboolean
gst_cel_video_src_set_caps (GstBaseSrc * basesrc, GstCaps * caps)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (basesrc);
  GstVideoFormat video_format;
  gint width, height, fps_n, fps_d;
  guint i;
  GstCelVideoFormat *selected_format;

  if (self->device == NULL)
    goto no_device;

  if (!gst_video_format_parse_caps (caps, &video_format, &width, &height))
    goto invalid_format;
  if (!gst_video_parse_caps_framerate (caps, &fps_n, &fps_d))
    goto invalid_format;

  gst_cel_video_src_ensure_device_caps_and_formats (self);

  selected_format = NULL;

  for (i = 0; i != self->device_formats->len; i++) {
    GstCelVideoFormat *format;

    format = &g_array_index (self->device_formats, GstCelVideoFormat, i);
    if (format->video_format == video_format &&
        format->width == width && format->height == height &&
        format->fps_n == fps_n && format->fps_d == fps_d) {
      selected_format = format;
      break;
    }
  }

  if (selected_format == NULL)
    goto invalid_format;

  GST_DEBUG_OBJECT (self, "selecting format %u", selected_format->index);

  if (!gst_cel_video_src_select_format (self, selected_format))
    goto select_failed;

  gst_cel_video_src_release_device_caps_and_formats (self);

  return TRUE;

  /* ERRORS */
no_device:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("no device"), (NULL));
    return FALSE;
  }
invalid_format:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("invalid format"), (NULL));
    return FALSE;
  }
select_failed:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("failed to select format"),
        (NULL));
    return FALSE;
  }
}

static gboolean
gst_cel_video_src_start (GstBaseSrc * basesrc)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (basesrc);

  g_atomic_int_set (&self->is_running, TRUE);
  self->offset = 0;

  self->last_sampling = GST_CLOCK_TIME_NONE;
  self->count = 0;
  self->fps = -1;

  return TRUE;
}

static gboolean
gst_cel_video_src_stop (GstBaseSrc * basesrc)
{
  return TRUE;
}

static gboolean
gst_cel_video_src_query (GstBaseSrc * basesrc, GstQuery * query)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (basesrc);
  gboolean result = FALSE;

  switch (GST_QUERY_TYPE (query)) {
    case GST_QUERY_LATENCY:{
      GstClockTime min_latency, max_latency;

      if (self->device == NULL || !GST_CLOCK_TIME_IS_VALID (self->duration))
        goto beach;

      min_latency = max_latency = self->duration;

      GST_DEBUG_OBJECT (self, "reporting latency of min %" GST_TIME_FORMAT
          " max %" GST_TIME_FORMAT,
          GST_TIME_ARGS (min_latency), GST_TIME_ARGS (max_latency));

      gst_query_set_latency (query, TRUE, min_latency, max_latency);
      result = TRUE;
      break;
    }
    default:
      result = GST_BASE_SRC_CLASS (parent_class)->query (basesrc, query);
      break;
  }

beach:
  return result;
}

static gboolean
gst_cel_video_src_unlock (GstBaseSrc * basesrc)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (basesrc);

  g_atomic_int_set (&self->is_running, FALSE);

  QUEUE_READY_LOCK (self);
  QUEUE_READY_NOTIFY (self);
  QUEUE_READY_UNLOCK (self);

  return TRUE;
}

static gboolean
gst_cel_video_src_unlock_stop (GstBaseSrc * basesrc)
{
  return TRUE;
}

static void
gst_cel_video_src_on_queue_ready (void *triggerRefcon,
    CMBufferQueueTriggerToken triggerToken)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (triggerRefcon);

  QUEUE_READY_LOCK (self);
  self->queue_is_ready = TRUE;
  QUEUE_READY_NOTIFY (self);
  QUEUE_READY_UNLOCK (self);
}

static void
gst_cel_video_src_timestamp_buffer (GstCelVideoSrc * self, GstBuffer * buf)
{
  GstClock *clock;
  GstClockTime ts;

  GST_OBJECT_LOCK (self);
  if ((clock = GST_ELEMENT_CLOCK (self)) != NULL) {
    ts = gst_clock_get_time (clock);

    if (ts > GST_ELEMENT (self)->base_time)
      ts -= GST_ELEMENT (self)->base_time;
    else
      ts = 0;

    if (ts > self->duration)
      ts -= self->duration;
    else
      ts = 0;
  } else {
    ts = GST_CLOCK_TIME_NONE;
  }
  GST_OBJECT_UNLOCK (self);

  GST_BUFFER_OFFSET (buf) = self->offset;
  GST_BUFFER_OFFSET_END (buf) = self->offset + 1;
  GST_BUFFER_TIMESTAMP (buf) = ts;
  GST_BUFFER_DURATION (buf) = self->duration;

  if (self->offset == 0)
    GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);

  self->offset++;
}

static void
gst_cel_video_src_update_statistics (GstCelVideoSrc * self)
{
  GstClock *clock;

  GST_OBJECT_LOCK (self);
  clock = GST_ELEMENT_CLOCK (self);
  if (clock != NULL)
    gst_object_ref (clock);
  GST_OBJECT_UNLOCK (self);

  if (clock != NULL) {
    GstClockTime now = gst_clock_get_time (clock);
    gst_object_unref (clock);

    self->count++;

    if (GST_CLOCK_TIME_IS_VALID (self->last_sampling)) {
      if (now - self->last_sampling >= GST_SECOND) {
        GST_OBJECT_LOCK (self);
        self->fps = self->count;
        GST_OBJECT_UNLOCK (self);

        g_object_notify (G_OBJECT (self), "fps");

        self->last_sampling = now;
        self->count = 0;
      }
    } else {
      self->last_sampling = now;
    }
  }
}

static GstFlowReturn
gst_cel_video_src_create (GstPushSrc * pushsrc, GstBuffer ** buf)
{
  GstCelVideoSrc *self = GST_CEL_VIDEO_SRC_CAST (pushsrc);
  GstCMApi *cm = self->ctx->cm;
  CMSampleBufferRef sbuf;

  sbuf = cm->CMBufferQueueDequeueAndRetain (self->queue);

  while (sbuf == NULL) {
    QUEUE_READY_LOCK (self);
    while (!self->queue_is_ready && g_atomic_int_get (&self->is_running))
      QUEUE_READY_WAIT (self);
    self->queue_is_ready = FALSE;
    QUEUE_READY_UNLOCK (self);

    if (G_UNLIKELY (!g_atomic_int_get (&self->is_running)))
      goto shutting_down;

    sbuf = cm->CMBufferQueueDequeueAndRetain (self->queue);
  }

  if (G_UNLIKELY (!g_atomic_int_get (&self->is_running)))
    goto shutting_down;

  *buf = gst_core_media_buffer_new (self->ctx, sbuf);
  gst_cel_video_src_timestamp_buffer (self, *buf);

  cm->FigSampleBufferRelease (sbuf);

  if (self->do_stats)
    gst_cel_video_src_update_statistics (self);

  return GST_FLOW_OK;

  /* ERRORS */
shutting_down:
  {
    cm->FigSampleBufferRelease (sbuf);
    return GST_FLOW_WRONG_STATE;
  }
}

static gboolean
gst_cel_video_src_open_device (GstCelVideoSrc * self)
{
  GstCoreMediaCtx *ctx = NULL;
  GError *error = NULL;
  GstCMApi *cm = NULL;
  GstMTApi *mt = NULL;
  GstCelApi *cel = NULL;
  OSStatus status;
  FigCaptureDeviceRef device = NULL;
  FigBaseObjectRef device_base;
  FigBaseVTable *device_vt;
  CFArrayRef stream_array = NULL;
  CFIndex stream_index;
  FigCaptureStreamRef stream = NULL;
  FigBaseObjectRef stream_base;
  FigBaseVTable *stream_vt;
  CMBufferQueueRef queue = NULL;
  CMTime ignored_time;

  ctx = gst_core_media_ctx_new (GST_API_CORE_VIDEO | GST_API_CORE_MEDIA
      | GST_API_MEDIA_TOOLBOX | GST_API_CELESTIAL, &error);
  if (error != NULL)
    goto api_error;
  cm = ctx->cm;
  mt = ctx->mt;
  cel = ctx->cel;

  status = cel->FigCreateCaptureDevicesAndStreamsForPreset (NULL,
      *(cel->kFigRecorderCapturePreset_VideoRecording), NULL,
      &device, &stream, NULL, NULL);
  if (status == kCelError_ResourceBusy)
    goto device_busy;
  else if (status != noErr)
    goto unexpected_error;

  device_base = mt->FigCaptureDeviceGetFigBaseObject (device);
  device_vt = cm->FigBaseObjectGetVTable (device_base);

  status = device_vt->base->CopyProperty (device_base,
      *(mt->kFigCaptureDeviceProperty_StreamArray), NULL,
      (CFTypeRef *) & stream_array);
  if (status != noErr)
    goto unexpected_error;

  if (self->device_index >= 0)
    stream_index = self->device_index;
  else
    stream_index = 0;

  if (stream_index >= CFArrayGetCount (stream_array))
    goto invalid_device_index;

  CFRelease (stream);
  stream = (FigCaptureStreamRef) CFArrayGetValueAtIndex (stream_array,
      stream_index);
  CFRetain (stream);

  stream_base = mt->FigCaptureStreamGetFigBaseObject (stream);
  stream_vt = cm->FigBaseObjectGetVTable (stream_base);

  status = stream_vt->base->CopyProperty (stream_base,
      *(mt->kFigCaptureStreamProperty_BufferQueue), NULL, &queue);
  if (status != noErr)
    goto unexpected_error;

  self->queue_is_ready = FALSE;

  ignored_time = cm->CMTimeMake (1, 1);
  status = cm->CMBufferQueueInstallTrigger (queue,
      gst_cel_video_src_on_queue_ready, self,
      kCMBufferQueueTrigger_WhenDataBecomesReady, ignored_time,
      &self->ready_trigger);
  if (status != noErr)
    goto unexpected_error;

  self->ctx = ctx;

  self->device = device;
  self->device_iface = device_vt->derived;
  self->device_base = device_base;
  self->device_base_iface = device_vt->base;
  self->stream = stream;
  self->stream_iface = stream_vt->derived;
  self->stream_base = stream_base;
  self->stream_base_iface = stream_vt->base;

  self->queue = queue;
  self->duration = GST_CLOCK_TIME_NONE;

  CFRelease (stream_array);

  return TRUE;

  /* ERRORS */
api_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("API error"),
        ("%s", error->message));
    g_clear_error (&error);
    goto any_error;
  }
device_busy:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, BUSY,
        ("device is already in use"), (NULL));
    goto any_error;
  }
invalid_device_index:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
        ("invalid video capture device index"), (NULL));
    goto any_error;
  }
unexpected_error:
  {
    GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
        ("unexpected error while opening device (%d)", (gint) status), (NULL));
    goto any_error;
  }
any_error:
  {
    if (stream != NULL)
      CFRelease (stream);
    if (stream_array != NULL)
      CFRelease (stream_array);
    if (device != NULL)
      CFRelease (device);

    if (ctx != NULL) {
      cm->FigBufferQueueRelease (queue);
      g_object_unref (ctx);
    }

    return FALSE;
  }
}

static void
gst_cel_video_src_close_device (GstCelVideoSrc * self)
{
  gst_cel_video_src_release_device_caps_and_formats (self);

  self->stream_iface->Stop (self->stream);
  self->stream_base_iface->Finalize (self->stream_base);
  CFRelease (self->stream);
  self->stream = NULL;
  self->stream_iface = NULL;
  self->stream_base = NULL;
  self->stream_base_iface = NULL;

  self->device_base_iface->Finalize (self->device_base);
  CFRelease (self->device);
  self->device = NULL;
  self->device_iface = NULL;
  self->device_base = NULL;
  self->device_base_iface = NULL;

  self->ctx->cm->CMBufferQueueRemoveTrigger (self->queue, self->ready_trigger);
  self->ctx->cm->FigBufferQueueRelease (self->queue);
  self->ready_trigger = NULL;
  self->queue = NULL;

  g_object_unref (self->ctx);
  self->ctx = NULL;
}

static void
gst_cel_video_src_ensure_device_caps_and_formats (GstCelVideoSrc * self)
{
  OSStatus status;
  CFArrayRef stream_formats = NULL;
  CFIndex format_count, i;

  if (self->device_caps != NULL)
    goto already_probed;

  self->device_caps = gst_caps_new_empty ();
  self->device_formats = g_array_new (FALSE, FALSE, sizeof (GstCelVideoFormat));

  status = self->stream_base_iface->CopyProperty (self->stream_base,
      *(self->ctx->mt->kFigCaptureStreamProperty_SupportedFormatsArray),
      NULL, (CFTypeRef *) & stream_formats);
  if (status != noErr)
    goto beach;

  format_count = CFArrayGetCount (stream_formats);
  GST_DEBUG_OBJECT (self, "device supports %d formats", (gint) format_count);

  for (i = 0; i != format_count; i++) {
    CFDictionaryRef sformat;
    GstCelVideoFormat format;

    sformat = CFArrayGetValueAtIndex (stream_formats, i);

    if (gst_cel_video_src_parse_stream_format (self, i, sformat, &format)) {
      gst_caps_append_structure (self->device_caps,
          gst_structure_new ("video/x-raw-yuv",
              "format", GST_TYPE_FOURCC, format.fourcc,
              "width", G_TYPE_INT, format.width,
              "height", G_TYPE_INT, format.height,
              "framerate", GST_TYPE_FRACTION, format.fps_n, format.fps_d,
              "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1, NULL));
      g_array_append_val (self->device_formats, format);
    } else {
      GST_WARNING_OBJECT (self, "ignoring unknown format #%d", (gint) i);
    }
  }

  CFRelease (stream_formats);

already_probed:
beach:
  return;
}

static void
gst_cel_video_src_release_device_caps_and_formats (GstCelVideoSrc * self)
{
  if (self->device_caps != NULL) {
    gst_caps_unref (self->device_caps);
    self->device_caps = NULL;
  }

  if (self->device_formats != NULL) {
    g_array_free (self->device_formats, TRUE);
    self->device_formats = NULL;
  }
}

static gboolean
gst_cel_video_src_select_format (GstCelVideoSrc * self,
    GstCelVideoFormat * format)
{
  gboolean result = FALSE;
  GstMTApi *mt = self->ctx->mt;
  GstCelApi *cel = self->ctx->cel;
  OSStatus status;
  SInt32 framerate;

  status = gst_cel_video_src_set_stream_property_i32 (self,
      *(mt->kFigCaptureStreamProperty_FormatIndex), format->index);
  if (status != noErr)
    goto beach;

  framerate = format->fps_n / format->fps_d;

  status = gst_cel_video_src_set_stream_property_i32 (self,
      *(mt->kFigCaptureStreamProperty_MinimumFrameRate), framerate);
  if (status != noErr)
    goto beach;

  status = gst_cel_video_src_set_stream_property_i32 (self,
      *(mt->kFigCaptureStreamProperty_MaximumFrameRate), framerate);
  if (status != noErr)
    goto beach;

  status = gst_cel_video_src_set_stream_property_value (self,
      *(cel->kFigCaptureStreamProperty_ColorRange),
      *(cel->kFigCapturePropertyValue_ColorRangeSDVideo));
  if (status != noErr)
    goto beach;

  status = self->stream_iface->Start (self->stream);
  if (status != noErr)
    goto beach;

  GST_DEBUG_OBJECT (self, "configured format %d (%d x %d @ %d Hz)",
      format->index, format->width, format->height, (gint) framerate);

  self->duration =
      gst_util_uint64_scale (GST_SECOND, format->fps_d, format->fps_n);

  result = TRUE;

beach:
  return result;
}

static gboolean
gst_cel_video_src_parse_stream_format (GstCelVideoSrc * self,
    guint index, CFDictionaryRef stream_format, GstCelVideoFormat * format)
{
  GstCMApi *cm = self->ctx->cm;
  GstMTApi *mt = self->ctx->mt;
  CMFormatDescriptionRef desc;
  CMVideoDimensions dim;
  UInt32 subtype;
  CFNumberRef framerate_value;
  SInt32 fps_n;

  format->index = index;

  desc = CFDictionaryGetValue (stream_format,
      *(mt->kFigSupportedFormat_FormatDescription));

  dim = cm->CMVideoFormatDescriptionGetDimensions (desc);
  format->width = dim.width;
  format->height = dim.height;

  subtype = cm->CMFormatDescriptionGetMediaSubType (desc);

  switch (subtype) {
    case kComponentVideoUnsigned:
      format->video_format = GST_VIDEO_FORMAT_YUY2;
      format->fourcc = GST_MAKE_FOURCC ('Y', 'U', 'Y', '2');
      break;
    case kYUV420vCodecType:
      format->video_format = GST_VIDEO_FORMAT_NV12;
      format->fourcc = GST_MAKE_FOURCC ('N', 'V', '1', '2');
      break;
    default:
      goto unsupported_format;
  }

  framerate_value = CFDictionaryGetValue (stream_format,
      *(mt->kFigSupportedFormat_VideoMaxFrameRate));
  CFNumberGetValue (framerate_value, kCFNumberSInt32Type, &fps_n);
  format->fps_n = fps_n;
  format->fps_d = 1;

  return TRUE;

unsupported_format:
  return FALSE;
}

static OSStatus
gst_cel_video_src_set_stream_property_i32 (GstCelVideoSrc * self,
    CFStringRef name, SInt32 value)
{
  OSStatus status;
  CFNumberRef number;

  number = CFNumberCreate (NULL, kCFNumberSInt32Type, &value);
  status = self->stream_base_iface->SetProperty (self->stream_base, name,
      number);
  CFRelease (number);

  return status;
}

static OSStatus
gst_cel_video_src_set_stream_property_value (GstCelVideoSrc * self,
    CFStringRef name, CFTypeRef value)
{
  return self->stream_base_iface->SetProperty (self->stream_base, name, value);
}

static void
gst_cel_video_src_base_init (gpointer gclass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (gclass);

  gst_element_class_set_details_simple (element_class,
      "Video Source (Celestial)", "Source/Video",
      "Reads frames from an iOS Celestial device",
      "Ole André Vadla Ravnås <oravnas@cisco.com>");

  gst_element_class_add_static_pad_template (element_class, &src_template);
}

static void
gst_cel_video_src_class_init (GstCelVideoSrcClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
  GstBaseSrcClass *gstbasesrc_class = GST_BASE_SRC_CLASS (klass);
  GstPushSrcClass *gstpushsrc_class = GST_PUSH_SRC_CLASS (klass);

  gobject_class->dispose = gst_cel_video_src_dispose;
  gobject_class->finalize = gst_cel_video_src_finalize;
  gobject_class->get_property = gst_cel_video_src_get_property;
  gobject_class->set_property = gst_cel_video_src_set_property;

  gstelement_class->change_state = gst_cel_video_src_change_state;

  gstbasesrc_class->get_caps = gst_cel_video_src_get_caps;
  gstbasesrc_class->set_caps = gst_cel_video_src_set_caps;
  gstbasesrc_class->start = gst_cel_video_src_start;
  gstbasesrc_class->stop = gst_cel_video_src_stop;
  gstbasesrc_class->query = gst_cel_video_src_query;
  gstbasesrc_class->unlock = gst_cel_video_src_unlock;
  gstbasesrc_class->unlock_stop = gst_cel_video_src_unlock_stop;

  gstpushsrc_class->create = gst_cel_video_src_create;

  g_object_class_install_property (gobject_class, PROP_DEVICE_INDEX,
      g_param_spec_int ("device-index", "Device Index",
          "The zero-based device index",
          -1, G_MAXINT, DEFAULT_DEVICE_INDEX,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_DO_STATS,
      g_param_spec_boolean ("do-stats", "Enable statistics",
          "Enable logging of statistics", DEFAULT_DO_STATS,
          G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_FPS,
      g_param_spec_int ("fps", "Frames per second",
          "Last measured framerate, if statistics are enabled",
          -1, G_MAXINT, -1, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  GST_DEBUG_CATEGORY_INIT (gst_cel_video_src_debug, "celvideosrc",
      0, "iOS Celestial video source");
}
