/* * Copyright (C) 2009 Ole André Vadla Ravnås * 2009 Knut Inge Hvidsten * * 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., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "miovideodevice.h" #include #include GST_DEBUG_CATEGORY_EXTERN (gst_mio_video_src_debug); #define GST_CAT_DEFAULT gst_mio_video_src_debug enum { PROP_0, PROP_CONTEXT, PROP_HANDLE, PROP_UID, PROP_NAME, PROP_TRANSPORT }; G_DEFINE_TYPE (GstMIOVideoDevice, gst_mio_video_device, G_TYPE_OBJECT); typedef struct _GstMIOVideoFormat GstMIOVideoFormat; typedef struct _GstMIOSetFormatCtx GstMIOSetFormatCtx; typedef struct _GstMIOFindRateCtx GstMIOFindRateCtx; struct _GstMIOVideoFormat { TundraObjectID stream; CMFormatDescriptionRef desc; UInt32 type; CMVideoDimensions dim; }; struct _GstMIOSetFormatCtx { UInt32 format; gint width, height; gint fps_n, fps_d; gboolean success; }; struct _GstMIOFindRateCtx { gdouble needle; gdouble closest_match; gboolean success; }; static void gst_mio_video_device_collect_format (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data); static GstStructure *gst_mio_video_device_format_basics_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format); static gboolean gst_mio_video_device_add_framerates_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s); static void gst_mio_video_device_add_pixel_aspect_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s); static void gst_mio_video_device_append_framerate (GstMIOVideoDevice * self, GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data); static void gst_mio_video_device_framerate_to_fraction_value (TundraFramerate * rate, GValue * fract); static gdouble gst_mio_video_device_round_to_whole_hundreths (gdouble value); static void gst_mio_video_device_guess_pixel_aspect_ratio (gint width, gint height, gint * par_width, gint * par_height); static void gst_mio_video_device_activate_matching_format (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data); static void gst_mio_video_device_find_closest_framerate (GstMIOVideoDevice * self, GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data); typedef void (*GstMIOVideoDeviceEachFormatFunc) (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data); typedef void (*GstMIOVideoDeviceEachFramerateFunc) (GstMIOVideoDevice * self, GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data); static void gst_mio_video_device_formats_foreach (GstMIOVideoDevice * self, GstMIOVideoDeviceEachFormatFunc func, gpointer user_data); static void gst_mio_video_device_format_framerates_foreach (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstMIOVideoDeviceEachFramerateFunc func, gpointer user_data); static gint gst_mio_video_device_compare (GstMIOVideoDevice * a, GstMIOVideoDevice * b); static gint gst_mio_video_device_calculate_score (GstMIOVideoDevice * device); static void gst_mio_video_device_init (GstMIOVideoDevice * self) { } static void gst_mio_video_device_dispose (GObject * object) { GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE_CAST (object); if (self->cached_caps != NULL) { gst_caps_unref (self->cached_caps); self->cached_caps = NULL; } G_OBJECT_CLASS (gst_mio_video_device_parent_class)->dispose (object); } static void gst_mio_video_device_finalize (GObject * object) { GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE_CAST (object); g_free (self->cached_uid); g_free (self->cached_name); G_OBJECT_CLASS (gst_mio_video_device_parent_class)->finalize (object); } static void gst_mio_video_device_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE (object); switch (prop_id) { case PROP_CONTEXT: g_value_set_pointer (value, self->ctx); break; case PROP_HANDLE: g_value_set_int (value, gst_mio_video_device_get_handle (self)); break; case PROP_UID: g_value_set_string (value, gst_mio_video_device_get_uid (self)); break; case PROP_NAME: g_value_set_string (value, gst_mio_video_device_get_name (self)); break; case PROP_TRANSPORT: g_value_set_uint (value, gst_mio_video_device_get_transport_type (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_mio_video_device_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstMIOVideoDevice *self = GST_MIO_VIDEO_DEVICE (object); switch (prop_id) { case PROP_CONTEXT: self->ctx = g_value_get_pointer (value); break; case PROP_HANDLE: self->handle = g_value_get_int (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } TundraObjectID gst_mio_video_device_get_handle (GstMIOVideoDevice * self) { return self->handle; } const gchar * gst_mio_video_device_get_uid (GstMIOVideoDevice * self) { if (self->cached_uid == NULL) { TundraTargetSpec pspec = { 0, }; pspec.name = kTundraObjectPropertyUID; pspec.scope = kTundraScopeGlobal; self->cached_uid = gst_mio_object_get_string (self->handle, &pspec, self->ctx->mio); } return self->cached_uid; } const gchar * gst_mio_video_device_get_name (GstMIOVideoDevice * self) { if (self->cached_name == NULL) { TundraTargetSpec pspec = { 0, }; pspec.name = kTundraObjectPropertyName; pspec.scope = kTundraScopeGlobal; self->cached_name = gst_mio_object_get_string (self->handle, &pspec, self->ctx->mio); } return self->cached_name; } TundraDeviceTransportType gst_mio_video_device_get_transport_type (GstMIOVideoDevice * self) { if (self->cached_transport == kTundraDeviceTransportInvalid) { TundraTargetSpec pspec = { 0, }; pspec.name = kTundraDevicePropertyTransportType; pspec.scope = kTundraScopeGlobal; self->cached_transport = gst_mio_object_get_uint32 (self->handle, &pspec, self->ctx->mio); } return self->cached_transport; } gboolean gst_mio_video_device_open (GstMIOVideoDevice * self) { /* nothing for now */ return TRUE; } void gst_mio_video_device_close (GstMIOVideoDevice * self) { /* nothing for now */ } GstCaps * gst_mio_video_device_get_available_caps (GstMIOVideoDevice * self) { if (self->cached_caps == NULL) { GstCaps *caps; caps = gst_caps_new_empty (); gst_mio_video_device_formats_foreach (self, gst_mio_video_device_collect_format, caps); self->cached_caps = caps; } return self->cached_caps; } static void gst_mio_video_device_collect_format (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data) { GstCaps *caps = user_data; GstStructure *s; s = gst_mio_video_device_format_basics_to_structure (self, format); if (s == NULL) goto unsupported_format; if (!gst_mio_video_device_add_framerates_to_structure (self, format, s)) goto no_framerates; gst_mio_video_device_add_pixel_aspect_to_structure (self, format, s); gst_caps_append_structure (caps, s); return; /* ERRORS */ unsupported_format: { gchar *fcc; fcc = gst_mio_fourcc_to_string (format->type); GST_WARNING ("skipping unsupported format %s", fcc); g_free (fcc); return; } no_framerates: { GST_WARNING ("no framerates?"); gst_structure_free (s); return; } } static GstStructure * gst_mio_video_device_format_basics_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format) { GstStructure *s; switch (format->type) { case kCVPixelFormatType_422YpCbCr8: case kCVPixelFormatType_422YpCbCr8Deprecated: { guint fcc; if (format->type == kCVPixelFormatType_422YpCbCr8) fcc = GST_MAKE_FOURCC ('U', 'Y', 'V', 'Y'); else fcc = GST_MAKE_FOURCC ('Y', 'U', 'Y', '2'); s = gst_structure_new ("video/x-raw-yuv", "format", GST_TYPE_FOURCC, fcc, "width", G_TYPE_INT, format->dim.width, "height", G_TYPE_INT, format->dim.height, NULL); break; } case kFigVideoCodecType_JPEG_OpenDML: { s = gst_structure_new ("image/jpeg", "width", G_TYPE_INT, format->dim.width, "height", G_TYPE_INT, format->dim.height, NULL); break; } default: s = NULL; break; } return s; } static gboolean gst_mio_video_device_add_framerates_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s) { GValue rates = { 0, }; const GValue *rates_value; g_value_init (&rates, GST_TYPE_LIST); gst_mio_video_device_format_framerates_foreach (self, format, gst_mio_video_device_append_framerate, &rates); if (gst_value_list_get_size (&rates) == 0) goto no_framerates; if (gst_value_list_get_size (&rates) > 1) rates_value = &rates; else rates_value = gst_value_list_get_value (&rates, 0); gst_structure_set_value (s, "framerate", rates_value); g_value_unset (&rates); return TRUE; /* ERRORS */ no_framerates: { g_value_unset (&rates); return FALSE; } } static void gst_mio_video_device_add_pixel_aspect_to_structure (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstStructure * s) { gint par_width, par_height; gst_mio_video_device_guess_pixel_aspect_ratio (format->dim.width, format->dim.height, &par_width, &par_height); gst_structure_set (s, "pixel-aspect-ratio", GST_TYPE_FRACTION, par_width, par_height, NULL); } static void gst_mio_video_device_append_framerate (GstMIOVideoDevice * self, GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data) { GValue *rates = user_data; GValue value = { 0, }; g_value_init (&value, GST_TYPE_FRACTION); gst_mio_video_device_framerate_to_fraction_value (rate, &value); gst_value_list_append_value (rates, &value); g_value_unset (&value); } static void gst_mio_video_device_framerate_to_fraction_value (TundraFramerate * rate, GValue * fract) { gdouble rounded; gint n, d; rounded = gst_mio_video_device_round_to_whole_hundreths (rate->value); gst_util_double_to_fraction (rounded, &n, &d); gst_value_set_fraction (fract, n, d); } static gdouble gst_mio_video_device_round_to_whole_hundreths (gdouble value) { gdouble m, x, y, z; m = 0.01; x = value; y = floor ((x / m) + 0.5); z = y * m; return z; } static void gst_mio_video_device_guess_pixel_aspect_ratio (gint width, gint height, gint * par_width, gint * par_height) { /* * As we dont have access to the actual pixel aspect, we will try to do a * best-effort guess. The guess is based on most sensors being either 4/3 * or 16/9, and most pixel aspects being close to 1/1. */ if (width == 768 && height == 448) { /* special case for w448p */ *par_width = 28; *par_height = 27; } else { if (((gdouble) width / (gdouble) height) < 1.2778) { *par_width = 12; *par_height = 11; } else { *par_width = 1; *par_height = 1; } } } gboolean gst_mio_video_device_set_caps (GstMIOVideoDevice * self, GstCaps * caps) { GstVideoFormat format; GstMIOSetFormatCtx ctx = { 0, }; if (gst_video_format_parse_caps (caps, &format, &ctx.width, &ctx.height)) { if (format == GST_VIDEO_FORMAT_UYVY) ctx.format = kCVPixelFormatType_422YpCbCr8; else if (format == GST_VIDEO_FORMAT_YUY2) ctx.format = kCVPixelFormatType_422YpCbCr8Deprecated; else g_assert_not_reached (); } else { GstStructure *s; s = gst_caps_get_structure (caps, 0); g_assert (gst_structure_has_name (s, "image/jpeg")); gst_structure_get_int (s, "width", &ctx.width); gst_structure_get_int (s, "height", &ctx.height); ctx.format = kFigVideoCodecType_JPEG_OpenDML; } gst_video_parse_caps_framerate (caps, &ctx.fps_n, &ctx.fps_d); gst_mio_video_device_formats_foreach (self, gst_mio_video_device_activate_matching_format, &ctx); return ctx.success; } static void gst_mio_video_device_activate_matching_format (GstMIOVideoDevice * self, GstMIOVideoFormat * format, gpointer user_data) { GstMIOSetFormatCtx *ctx = user_data; GstMIOFindRateCtx find_ctx; TundraTargetSpec spec = { 0, }; TundraStatus status; if (format->type != ctx->format) return; else if (format->dim.width != ctx->width) return; else if (format->dim.height != ctx->height) return; find_ctx.needle = (gdouble) ctx->fps_n / (gdouble) ctx->fps_d; find_ctx.closest_match = 0.0; find_ctx.success = FALSE; gst_mio_video_device_format_framerates_foreach (self, format, gst_mio_video_device_find_closest_framerate, &find_ctx); if (!find_ctx.success) goto no_matching_framerate_found; spec.scope = kTundraScopeInput; spec.name = kTundraStreamPropertyFormatDescription; status = self->ctx->mio->TundraObjectSetPropertyData (format->stream, &spec, NULL, NULL, sizeof (format->desc), &format->desc); if (status != kTundraSuccess) goto failed_to_set_format; spec.name = kTundraStreamPropertyFrameRate; status = self->ctx->mio->TundraObjectSetPropertyData (format->stream, &spec, NULL, NULL, sizeof (find_ctx.closest_match), &find_ctx.closest_match); if (status != kTundraSuccess) goto failed_to_set_framerate; self->selected_format = format->desc; self->selected_fps_n = ctx->fps_n; self->selected_fps_d = ctx->fps_d; ctx->success = TRUE; return; /* ERRORS */ no_matching_framerate_found: { GST_ERROR ("no matching framerate found"); return; } failed_to_set_format: { GST_ERROR ("failed to set format: 0x%08x", status); return; } failed_to_set_framerate: { GST_ERROR ("failed to set framerate: 0x%08x", status); return; } } static void gst_mio_video_device_find_closest_framerate (GstMIOVideoDevice * self, GstMIOVideoFormat * format, TundraFramerate * rate, gpointer user_data) { GstMIOFindRateCtx *ctx = user_data; if (fabs (rate->value - ctx->needle) <= 0.1) { ctx->closest_match = rate->value; ctx->success = TRUE; } } CMFormatDescriptionRef gst_mio_video_device_get_selected_format (GstMIOVideoDevice * self) { return self->selected_format; } GstClockTime gst_mio_video_device_get_duration (GstMIOVideoDevice * self) { return gst_util_uint64_scale_int (GST_SECOND, self->selected_fps_d, self->selected_fps_n); } static void gst_mio_video_device_formats_foreach (GstMIOVideoDevice * self, GstMIOVideoDeviceEachFormatFunc func, gpointer user_data) { GstCMApi *cm = self->ctx->cm; GstMIOApi *mio = self->ctx->mio; TundraTargetSpec spec = { 0, }; GArray *streams; guint stream_idx; spec.name = kTundraDevicePropertyStreams; spec.scope = kTundraScopeInput; streams = gst_mio_object_get_array (self->handle, &spec, sizeof (TundraObjectID), mio); /* TODO: We only consider the first stream for now */ for (stream_idx = 0; stream_idx != MIN (streams->len, 1); stream_idx++) { TundraObjectID stream; CFArrayRef formats; CFIndex num_formats, fmt_idx; stream = g_array_index (streams, TundraObjectID, stream_idx); spec.name = kTundraStreamPropertyFormatDescriptions; spec.scope = kTundraScopeInput; formats = gst_mio_object_get_pointer (stream, &spec, mio); num_formats = CFArrayGetCount (formats); for (fmt_idx = 0; fmt_idx != num_formats; fmt_idx++) { GstMIOVideoFormat fmt; fmt.stream = stream; fmt.desc = (CMFormatDescriptionRef) CFArrayGetValueAtIndex (formats, fmt_idx); if (cm->CMFormatDescriptionGetMediaType (fmt.desc) != kFigMediaTypeVideo) continue; fmt.type = cm->CMFormatDescriptionGetMediaSubType (fmt.desc); fmt.dim = cm->CMVideoFormatDescriptionGetDimensions (fmt.desc); func (self, &fmt, user_data); } } g_array_free (streams, TRUE); } static void gst_mio_video_device_format_framerates_foreach (GstMIOVideoDevice * self, GstMIOVideoFormat * format, GstMIOVideoDeviceEachFramerateFunc func, gpointer user_data) { TundraTargetSpec spec = { 0, }; GArray *rates; guint rate_idx; spec.name = kTundraStreamPropertyFrameRates; spec.scope = kTundraScopeInput; rates = gst_mio_object_get_array_full (format->stream, &spec, sizeof (format->desc), &format->desc, sizeof (TundraFramerate), self->ctx->mio); for (rate_idx = 0; rate_idx != rates->len; rate_idx++) { TundraFramerate *rate; rate = &g_array_index (rates, TundraFramerate, rate_idx); func (self, format, rate, user_data); } g_array_free (rates, TRUE); } void gst_mio_video_device_print_debug_info (GstMIOVideoDevice * self) { GstCMApi *cm = self->ctx->cm; GstMIOApi *mio = self->ctx->mio; TundraTargetSpec spec = { 0, }; gchar *str; GArray *streams; guint stream_idx; g_print ("Device %p with handle %d\n", self, self->handle); spec.scope = kTundraScopeGlobal; spec.name = kTundraObjectPropertyClass; str = gst_mio_object_get_fourcc (self->handle, &spec, mio); g_print (" Class: '%s'\n", str); g_free (str); spec.name = kTundraObjectPropertyCreator; str = gst_mio_object_get_string (self->handle, &spec, mio); g_print (" Creator: \"%s\"\n", str); g_free (str); spec.name = kTundraDevicePropertyModelUID; str = gst_mio_object_get_string (self->handle, &spec, mio); g_print (" Model UID: \"%s\"\n", str); g_free (str); spec.name = kTundraDevicePropertyTransportType; str = gst_mio_object_get_fourcc (self->handle, &spec, mio); g_print (" Transport Type: '%s'\n", str); g_free (str); g_print (" Streams:\n"); spec.name = kTundraDevicePropertyStreams; spec.scope = kTundraScopeInput; streams = gst_mio_object_get_array (self->handle, &spec, sizeof (TundraObjectID), mio); for (stream_idx = 0; stream_idx != streams->len; stream_idx++) { TundraObjectID stream; CFArrayRef formats; CFIndex num_formats, fmt_idx; stream = g_array_index (streams, TundraObjectID, stream_idx); g_print (" stream[%u] = %d\n", stream_idx, stream); spec.scope = kTundraScopeInput; spec.name = kTundraStreamPropertyFormatDescriptions; formats = gst_mio_object_get_pointer (stream, &spec, mio); num_formats = CFArrayGetCount (formats); g_print (" <%u formats>\n", (guint) num_formats); for (fmt_idx = 0; fmt_idx != num_formats; fmt_idx++) { CMFormatDescriptionRef fmt; gchar *media_type; gchar *media_sub_type; CMVideoDimensions dim; GArray *rates; guint rate_idx; fmt = CFArrayGetValueAtIndex (formats, fmt_idx); media_type = gst_mio_fourcc_to_string (cm->CMFormatDescriptionGetMediaType (fmt)); media_sub_type = gst_mio_fourcc_to_string (cm->CMFormatDescriptionGetMediaSubType (fmt)); dim = cm->CMVideoFormatDescriptionGetDimensions (fmt); g_print (" format[%u]: MediaType='%s' MediaSubType='%s' %ux%u\n", (guint) fmt_idx, media_type, media_sub_type, (guint) dim.width, (guint) dim.height); spec.name = kTundraStreamPropertyFrameRates; rates = gst_mio_object_get_array_full (stream, &spec, sizeof (fmt), &fmt, sizeof (TundraFramerate), mio); for (rate_idx = 0; rate_idx != rates->len; rate_idx++) { TundraFramerate *rate; rate = &g_array_index (rates, TundraFramerate, rate_idx); g_print (" %f\n", rate->value); } g_array_free (rates, TRUE); g_free (media_sub_type); g_free (media_type); } } g_array_free (streams, TRUE); } static void gst_mio_video_device_class_init (GstMIOVideoDeviceClass * klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = gst_mio_video_device_dispose; gobject_class->finalize = gst_mio_video_device_finalize; gobject_class->get_property = gst_mio_video_device_get_property; gobject_class->set_property = gst_mio_video_device_set_property; g_object_class_install_property (gobject_class, PROP_CONTEXT, g_param_spec_pointer ("context", "CoreMedia Context", "CoreMedia context to use", G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_HANDLE, g_param_spec_int ("handle", "Handle", "MIO handle of this video capture device", G_MININT, G_MAXINT, -1, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_UID, g_param_spec_string ("uid", "Unique ID", "Unique ID of this video capture device", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_NAME, g_param_spec_string ("name", "Device Name", "Name of this video capture device", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_TRANSPORT, g_param_spec_uint ("transport", "Transport", "Transport type of this video capture device", 0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); } GList * gst_mio_video_device_list_create (GstCoreMediaCtx * ctx) { GList *devices = NULL; TundraTargetSpec pspec = { 0, }; GArray *handles; guint handle_idx; pspec.name = kTundraSystemPropertyDevices; pspec.scope = kTundraScopeGlobal; handles = gst_mio_object_get_array (TUNDRA_SYSTEM_OBJECT_ID, &pspec, sizeof (TundraObjectID), ctx->mio); if (handles == NULL) goto beach; for (handle_idx = 0; handle_idx != handles->len; handle_idx++) { TundraObjectID handle; GstMIOVideoDevice *device; handle = g_array_index (handles, TundraObjectID, handle_idx); device = g_object_new (GST_TYPE_MIO_VIDEO_DEVICE, "context", ctx, "handle", handle, NULL); /* TODO: Skip screen input devices for now */ if (gst_mio_video_device_get_transport_type (device) != kTundraDeviceTransportScreen) { devices = g_list_prepend (devices, device); } else { g_object_unref (device); } } devices = g_list_sort (devices, (GCompareFunc) gst_mio_video_device_compare); g_array_free (handles, TRUE); beach: return devices; } void gst_mio_video_device_list_destroy (GList * devices) { g_list_foreach (devices, (GFunc) g_object_unref, NULL); g_list_free (devices); } static gint gst_mio_video_device_compare (GstMIOVideoDevice * a, GstMIOVideoDevice * b) { gint score_a, score_b; score_a = gst_mio_video_device_calculate_score (a); score_b = gst_mio_video_device_calculate_score (b); if (score_a > score_b) return -1; else if (score_a < score_b) return 1; return g_ascii_strcasecmp (gst_mio_video_device_get_name (a), gst_mio_video_device_get_name (b)); } static gint gst_mio_video_device_calculate_score (GstMIOVideoDevice * device) { switch (gst_mio_video_device_get_transport_type (device)) { case kTundraDeviceTransportScreen: return 0; case kTundraDeviceTransportBuiltin: return 1; case kTundraDeviceTransportUSB: return 2; default: return 3; } }