diff --git a/configure.ac b/configure.ac
index b5b8652..c2a7dfe 100644
--- a/configure.ac
+++ b/configure.ac
@@ -86,7 +86,7 @@ PKG_CHECK_MODULES([EXAMPLE_DEPS], [$RYGEL_COMMON_MODULES gio-2.0 >= $GIO_REQUIRE
 
 
 dnl Media engine to use
-BUILT_ENGINES="librygel-media-engine-simple"
+BUILT_ENGINES="librygel-media-engine-simple librygel-media-engine-odid"
 
 dnl Disabling the gstreamer media_engine also disables
 dnl anything else that uses gstreamer.
@@ -412,6 +449,8 @@ src/media-engines/gstreamer/Makefile
 src/media-engines/gstreamer/media-engine-gst.plugin
 src/media-engines/simple/Makefile
 src/media-engines/simple/media-engine-simple.plugin
+src/media-engines/odid/Makefile
+src/media-engines/odid/media-engine-odid.plugin
 src/plugins/Makefile
 data/Makefile
 data/xml/Makefile
diff --git a/data/rygel.conf b/data/rygel.conf
index 307da12..f2d1857 100644
--- a/data/rygel.conf
+++ b/data/rygel.conf
@@ -83,6 +86,30 @@ allow-deletion=true
 # List of active transcoders. To disable one, remove from list.
 transcoders=mp3;lpcm;mp2ts;wmv;aac;avc
 
+# The options shown in this configuration file are specific to the odid media engine.
+[OdidMediaEngine]
+
+# List of profile/mime/extensions to expose when using the CL-DLNA media engine 
+# Note: Currently all exported content must be compliant to these profiles
+# The listed profile is used in SupportedUploadProfiles. Empty when
+# upload is not enabled.
+profiles=
+
+# DTCP related properties
+# dtcp-enabled : This flag is a system-wide dtcp switch
+# dtcp-host    : DTCP host IP address
+# dtcp-port    : DTCP port(range between 6000 - 8999). Default port used is 8999
+# dtcp-storage : This will have to point to the DTCP keys directory which contains
+#                the certificates, and other keys for DTCP support.
+dtcp-enabled=false
+dtcp-host=
+dtcp-port=8999
+dtcp-storage=
+
+# Units are in KiloBytes (1KB = 1024 Bytes)
+# Default is set to 1.5MB = 1536 KB = 1536 * 1024 Bytes
+chunk-size=1536
+
 # Options that apply to the renderer framework in general
 [Renderer]
 
diff --git a/src/media-engines/Makefile.am b/src/media-engines/Makefile.am
index ed2f101..bc19684 100644
--- a/src/media-engines/Makefile.am
+++ b/src/media-engines/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS=simple
+SUBDIRS=simple odid
 
 if HAVE_GSTREAMER
 SUBDIRS += gstreamer
diff --git a/src/media-engines/odid/Makefile.am b/src/media-engines/odid/Makefile.am
new file mode 100644
index 0000000..5f30f68
--- /dev/null
+++ b/src/media-engines/odid/Makefile.am
@@ -0,0 +1,32 @@
+include $(top_srcdir)/common.am
+
+engine_LTLIBRARIES = libdtcpip.la librygel-media-engine-odid.la
+engine_DATA = media-engine-odid.plugin
+
+libdtcpip_la_SOURCES = \
+	dtcpip.h \
+	dtcpip_stub.c
+
+librygel_media_engine_odid_la_SOURCES = \
+	odid-media-engine.vala \
+	odid-util.vala \
+	odid-data-source.vala
+
+librygel_media_engine_odid_la_VALAFLAGS = \
+	$(RYGEL_COMMON_LIBRYGEL_SERVER_VALAFLAGS) \
+	$(RYGEL_COMMON_VALAFLAGS) \
+	--vapidir=./ --pkg dtcpip
+
+librygel_media_engine_odid_la_CFLAGS = \
+	$(LIBRYGEL_SERVER_DEPS_CFLAGS) \
+	$(RYGEL_COMMON_LIBRYGEL_SERVER_CFLAGS) \
+	-DG_LOG_DOMAIN='"MediaEngine-ODID"'
+librygel_media_engine_odid_la_LIBADD = \
+	$(LIBRYGEL_SERVER_DEPS_LIBS) \
+	$(RYGEL_COMMON_LIBRYGEL_SERVER_LIBS) \
+	libdtcpip.la
+librygel_media_engine_odid_la_LDFLAGS = \
+	$(RYGEL_PLUGIN_LINKER_FLAGS)
+
+EXTRA_DIST = media-engine-odid.plugin.in
+
diff --git a/src/media-engines/odid/dtcpip.h b/src/media-engines/odid/dtcpip.h
new file mode 100644
index 0000000..6eff680
--- /dev/null
+++ b/src/media-engines/odid/dtcpip.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (C) 2008-2013, Cable Television Laboratories, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, version 2. This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program.  If not, see  <http://www.gnu.org/licenses/>.
+ *
+ *  Please contact CableLabs if you need additional information or
+ *  have any questions.
+ *
+ *      CableLabs
+ *      858 Coal Creek Cir
+ *      Louisville, CO 80027-9750
+ *      303 661-9100
+ *      oc-mail@cablelabs.com
+ *
+ *  If you or the company you represent has a separate agreement with CableLabs
+ *  concerning the use of this code, your rights and obligations with respect
+ *  to this code shall be as set forth therein. No license is granted hereunder
+ *  for any other purpose.
+ */
+
+#ifndef __H_DTCPIP
+#define __H_DTCPIP
+
+unsigned int dtcpip_get_encrypted_sz (unsigned int cleartext_size, unsigned int base_pcp_payload);
+int dtcpip_cmn_init(char* pIPAndPort);
+
+int dtcpip_snk_init(void);
+int dtcpip_snk_open(char* ip_addr, unsigned short ip_port, int *session_handle);
+int dtcpip_snk_alloc_decrypt(int session_handle, char* encrypted_data, size_t encrypted_size,
+ char** cleartext_data, size_t* cleartext_size);
+int dtcpip_snk_free(char* cleartext_data);
+int dtcpip_snk_close(int session_handle);
+
+int dtcpip_src_init(unsigned short dtcp_port);
+int dtcpip_src_open(int* session_handle, int is_audio_only);
+int dtcpip_src_alloc_encrypt(int session_handle, unsigned char cci,
+char* cleartext_data, size_t cleartext_size,
+char** encrypted_data,size_t* encrypted_size);
+int dtcpip_src_free(char* encrypted_data);
+int dtcpip_src_close(int session_handle);
+int dtcpip_src_close_socket(int session_handle);
+
+int CVP2_DTCPIP_Init(char *pCertStorageDir);
+int CVP2_DTCPIP_GetLocalCert (unsigned char *pLocalCert, unsigned int *pLocalCertSize);
+int CVP2_DTCPIP_VerifyRemoteCert(unsigned char *pRemoteCert,  unsigned int nRemoteCertSz);
+int CVP2_DTCPIP_SignData( unsigned char *pData, unsigned int nDataSz,
+unsigned char *pSignature, unsigned int *pnSignatureSz);
+int CVP2_DTCPIP_VerifyData(unsigned char *pData, unsigned int nDataSz,
+unsigned char *pSignature, unsigned char *pRemoteCert );
+
+#endif
diff --git a/src/media-engines/odid/dtcpip.vapi b/src/media-engines/odid/dtcpip.vapi
new file mode 100644
index 0000000..ea15c8e
--- /dev/null
+++ b/src/media-engines/odid/dtcpip.vapi
@@ -0,0 +1,82 @@
+/*
+ *  Copyright (C) 2008-2013, Cable Television Laboratories, Inc.
+ *
+ *  Author: Parthiban Balasubramanian <P.Balasubramanian-contractor@cablelabs.com>
+ *
+ *  This file is part of Rygel.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, version 2. This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program.  If not, see  <http://www.gnu.org/licenses/>.
+ *
+ *  Please contact CableLabs if you need additional information or
+ *  have any questions.
+ *
+ *      CableLabs
+ *      858 Coal Creek Cir
+ *      Louisville, CO 80027-9750
+ *      303 661-9100
+ *      oc-mail@cablelabs.com
+ *
+ *  If you or the company you represent has a separate agreement with CableLabs
+ *  concerning the use of this code, your rights and obligations with respect
+ *  to this code shall be as set forth therein. No license is granted hereunder
+ *  for any other purpose.
+ */
+
+[CCode (cheader_filename = "dtcpip.h")]
+namespace Dtcpip {
+    //Common functions
+    [CCode (cname = "dtcpip_get_encrypted_sz")]
+    public static uint64 get_encrypted_length (uint64 cleartext_length, uint64 encrypted_length);
+    [CCode (cname = "dtcpip_cmn_init")]
+    public static int init_dtcp_library ([CCode (type = "char*")]string storage_path);
+//    [CCode (cname = "dtcpip_cmn_get_version")]
+//    public static void cmn_get_version(char* str, int length);
+
+    // Source Functions
+    [CCode (cname = "dtcpip_src_init")]
+    public static int server_dtcp_init (ushort dtcp_port);
+    [CCode (cname = "dtcpip_src_open")]
+    public static int server_dtcp_open (out int session_handle, int is_audio_only);
+    [CCode (cname = "dtcpip_src_alloc_encrypt")]
+    public static int server_dtcp_encrypt (int session_handle, uchar cci, [CCode (array_length_pos = 3.1)] uint8[] cleartext_data,
+                                           [CCode (array_length_pos = 5.1)] out unowned uint8[] encrypted_data);
+    [CCode (cname = "dtcpip_src_free")]
+    public static int server_dtcp_free ([CCode (array_length = false , type = "char*")] uint8[] encrypted_data);
+    [CCode (cname = "dtcpip_src_close")]
+    public static int server_dtcp_close (int session_handle);
+    [CCode (cname = "dtcpip_src_close_socket")]
+    public static int server_dtcp_close_socket (int session_handle);
+
+    //Sink Functions
+    [CCode (cname = "dtcpip_snk_init")]
+    public static int client_dtcp_init ();
+    [CCode (cname = "dtcpip_snk_open")]
+    int client_dtcp_open (string ip_addr, ushort ip_port, out int session_handle);
+    [CCode (cname = "dtcpip_snk_alloc_decrypt")]
+    public static int client_dtcp_decrypt (int session_handle, [CCode (array_length_pos = 2.1)] uint8[] encrypted_data,
+                                           [CCode (array_length_pos = 4.1)] out unowned uint8[] cleartext_data);
+    [CCode (cname = "dtcpip_snk_free")]
+    public static int client_dtcp_free ([CCode (array_length = false , type = "char*")] uint8[] cleartext_data);
+    [CCode (cname = "dtcpip_snk_close")]
+    public static int client_dtcp_close (int session_handle);
+
+    // CVP2 functions
+    [CCode (cname = "CVP2_DTCPIP_Init")]
+    public static int cvp2_dtcp_init(string pCertStorageDir);
+    [CCode (cname = "CVP2_DTCPIP_GetLocalCert")]
+    public static int get_local_cert (uchar *pLocalCert, [CCode (type = "unsigned int")] uint64 *pLocalCertSize);
+    [CCode (cname = "CVP2_DTCPIP_VerifyRemoteCert")]
+    public static int verify_remote_cert(uchar *pRemoteCert, [CCode (type = "unsigned int")] uint64 nRemoteCertSz);
+    [CCode (cname = "CVP2_DTCPIP_SignData")]
+    public static int sign_data(uchar *pData,[CCode (type = "unsigned int")] uint64 nDataSz, uchar *pSignature, [CCode (type = "unsigned int")] out uint64 pnSignatureSz);
+    [CCode (cname = "CVP2_DTCPIP_VerifyData")]
+    public static int verify_data(uchar *pData, [CCode (type = "unsigned int")] uint64 nDataSz, uchar *pSignature, uchar *pRemoteCert);
+}
diff --git a/src/media-engines/odid/dtcpip_stub.c b/src/media-engines/odid/dtcpip_stub.c
new file mode 100644
index 0000000..29dbfd4
--- /dev/null
+++ b/src/media-engines/odid/dtcpip_stub.c
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ *
+ * Author: Prasanna Modem <prasanna@ecaspia.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Author: Prasanna Modem <prasanna@ecaspia.com>
+ */
+#include <stdio.h>
+#include <string.h> // memcpy, strrchr, strlen
+
+#include "dtcpip.h"
+
+unsigned int dtcpip_get_encrypted_sz (unsigned int cleartext_size, unsigned int base_pcp_payload)
+{
+    return 0;
+}
+
+int dtcpip_cmn_init(char* pIPAndPort)
+{
+    return -1;
+}
+
+void dtcpip_cmn_get_version(char* string, size_t length)
+{
+
+}
+
+int dtcpip_src_init(unsigned short dtcp_port)
+{
+    return -1;
+}
+
+int dtcpip_src_open(int* session_handle, int is_audio_only)
+{
+    return -1;
+}
+
+int dtcpip_src_alloc_encrypt(int session_handle,
+                 unsigned char cci,
+                 char* cleartext_data, size_t cleartext_size,
+                 char** encrypted_data, size_t* encrypted_size)
+{
+    return -1;
+}
+
+int dtcpip_src_free(char* encrypted_data)
+{
+    return -1;
+}
+
+int dtcpip_src_close(int session_handle)
+{
+    return -1;
+}
+
+int dtcpip_snk_init(void)
+{
+    return -1;
+}
+
+int dtcpip_snk_open(
+                 char* ip_addr, unsigned short ip_port,
+                 int *session_handle)
+{
+    return -1;
+}
+
+int dtcpip_snk_alloc_decrypt(int session_handle,
+                 char* encrypted_data, size_t encrypted_size,
+                 char** cleartext_data, size_t* cleartext_size)
+{
+    return -1;
+}
+
+int dtcpip_snk_free(char* cleartext_data)
+{
+    return -1;
+}
+
+int dtcpip_snk_close(int session_handle)
+{
+    return -1;
+}
diff --git a/src/media-engines/odid/media-engine-odid.plugin.in b/src/media-engines/odid/media-engine-odid.plugin.in
new file mode 100644
index 0000000..60a77bc
--- /dev/null
+++ b/src/media-engines/odid/media-engine-odid.plugin.in
@@ -0,0 +1,7 @@
+[Plugin]
+Version = @VERSION@
+Module = media-engine-odid
+Name = media-engine-odid
+License = LGPL
+Description = CableLabs media streaming engine for on-disk media items (odid)
+Copyright = Copyright © CableLabs
diff --git a/src/media-engines/odid/odid-data-source.vala b/src/media-engines/odid/odid-data-source.vala
new file mode 100644
index 0000000..3f591a0
--- /dev/null
+++ b/src/media-engines/odid/odid-data-source.vala
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ *
+ * Author: Craig Pratt <craig@ecaspia.com>
+ *         Parthiban Balasubramanian <P.Balasubramanian-contractor@cablelabs.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Based on Rygel SimpleDataSource
+ * Copyright (C) 2012 Intel Corporation.
+ */
+
+/**
+ * A simple data source for use with the ODID media engine.
+ */
+using Dtcpip;
+using GUPnP;
+
+internal class Rygel.ODIDDataSource : DataSource, Object {
+    private string source_uri;
+    private Thread<void*> thread;
+    private string content_uri;
+    private Mutex mutex = Mutex ();
+    private Cond cond = Cond ();
+    private int64 range_start = 0;
+    private int64 range_end = 0; // non-inclusive
+    private bool frozen = false;
+    private bool stop_thread = false;
+    private HTTPSeekRequest seek_request;
+    private PlaySpeedRequest playspeed_request = null;
+    private MediaResource res;
+    private bool content_protected = false;
+    private int dtcp_session_handle = -1;
+
+    public ODIDDataSource (string source_uri, MediaResource ? res) {
+        debug ("Creating data source for %s resource %s", source_uri, res.get_name ());
+        this.source_uri = source_uri;
+        this.res = res;
+    }
+
+    ~ODIDDataSource () {
+        this.stop ();
+        this.clear_dtcp_session ();
+        debug ("Stopped data source");
+    }
+
+    public Gee.List<HTTPResponseElement> ? preroll ( HTTPSeekRequest? seek_request,
+                                                     PlaySpeedRequest? playspeed_request)
+       throws Error {
+        debug ("source uri: " + source_uri);
+
+        var response_list = new Gee.ArrayList<HTTPResponseElement> ();
+
+        this.seek_request = seek_request;
+        this.playspeed_request = playspeed_request;
+
+        if (res == null) {
+            throw new DataSourceError.GENERAL ("null resource");
+        }
+
+        debug ("Resource " + res.to_string ());
+        File odidItem = File.new_for_uri (source_uri);
+
+        KeyFile keyFile = new KeyFile ();
+        keyFile.load_from_file (odidItem.get_path (),
+                               KeyFileFlags.KEEP_COMMENTS |
+                               KeyFileFlags.KEEP_TRANSLATIONS);
+
+        string odid_item_path = null;
+        if (keyFile.has_key ("item", "odid_uri"))    {
+            odid_item_path = keyFile.get_string ("item", "odid_uri");
+        } else {
+            // If the odid_uri property is not available, assume this file exists in
+            // the correct directory.
+            if (odidItem.get_parent () != null) {
+                odid_item_path = odidItem.get_parent ().get_uri () + "/";
+            } else {
+                throw new DataSourceError.GENERAL ("Root level odid items not supported");
+            }
+        }
+
+        debug ("Source item path: %s", odid_item_path);
+
+        // The resources are published by this engine according to the resource directory name
+        //  i.e. the MediaResource "name" field was set to the directory name when
+        //  get_resources() was called
+        string resource_dir = res.get_name ();
+        string resource_path = odid_item_path + resource_dir + "/";
+        debug ("  resource directory: %s", resource_dir);
+
+        string basename = get_resource_property (resource_path,"basename");
+
+        string file_extension;
+        string content_filename = ODIDMediaEngine.content_filename_for_res_speed (
+                                                      odid_item_path + resource_dir,
+                                                      basename,
+                                                      (playspeed_request==null)
+                                                       ? null
+                                                       : playspeed_request.speed,
+                                                      out file_extension );
+        this.content_uri = resource_path + content_filename;
+        debug ("    content file: %s", content_filename);
+
+        this.content_protected = ( res.dlna_flags
+                                   & DLNAFlags.LINK_PROTECTED_CONTENT ) != 0;
+        if (this.content_protected) {
+            // Sanity check
+            if (!res.dlna_profile.has_prefix ("DTCP_")) {
+                throw new DataSourceError.GENERAL
+                              ("Request to stream protected content in non-protected profile: "
+                               + res.dlna_profile );
+            }
+            debug ("      Content is protected");
+        } else {
+            debug ("      Content is not protected");
+        }
+
+        // Get the size for the content file
+        File content_file = File.new_for_uri (this.content_uri);
+        FileInfo content_info = content_file.query_info (GLib.FileAttribute.STANDARD_SIZE, 0);
+        int64 total_size = content_info.get_size ();
+        debug ("      Total size is " + total_size.to_string ());
+
+        // Process PlaySpeed
+        if (playspeed_request != null) {
+            int framerate = 0;
+            string framerate_for_speed = get_content_property
+                                             (content_uri, "framerate");
+            if (framerate_for_speed == null) {
+                framerate = PlaySpeedResponse.NO_FRAMERATE;
+            } else {
+                framerate = int.parse ((framerate_for_speed == null)
+                                       ? "" : framerate_for_speed);
+                if (framerate == 0) {
+                    framerate = PlaySpeedResponse.NO_FRAMERATE;
+                }
+            }
+            debug ( "    framerate for speed %s: %s",
+                    playspeed_request.speed.to_string (),
+                    ( (framerate == PlaySpeedResponse.NO_FRAMERATE) ? "None"
+                      : framerate.to_string () ) );
+            var speed_response
+                 = new PlaySpeedResponse.from_speed ( playspeed_request.speed,
+                                                     (framerate > 0) ? framerate
+                                                     : PlaySpeedResponse.NO_FRAMERATE );
+            response_list.add (speed_response);
+        }
+
+        bool perform_cleartext_response;
+
+        // Process HTTPSeekRequest
+        if (seek_request == null) {
+            debug ("No seek request received");
+            perform_cleartext_response = false;
+        } else if (seek_request is HTTPTimeSeekRequest) {
+            //
+            // Time-based seek
+            //
+            var time_seek = seek_request as HTTPTimeSeekRequest;
+            bool is_reverse = (playspeed_request != null)
+                              && (!playspeed_request.speed.is_positive ());
+
+            // Calculate the effective range of the time seek using the appropriate index file
+
+            int64 time_offset_start = time_seek.start_time;
+            int64 time_offset_end;
+            if (time_seek.end_time == HTTPSeekRequest.UNSPECIFIED) {
+                // For time-seek, the "end" of the time range depends on the direction
+                time_offset_end = is_reverse ? 0 : int64.MAX;
+            } else {
+                time_offset_end = time_seek.end_time;
+            }
+
+            string index_path = resource_path + "/"
+                                + content_filename + ".index";
+            int64 total_duration = (time_seek.total_duration
+                                    != HTTPSeekRequest.UNSPECIFIED)
+                                   ? time_seek.total_duration : int64.MAX;
+            debug ("      Total duration is " + total_duration.to_string ());
+            debug ("Processing time seek (time %lldns to %lldns)",
+                   time_offset_start, time_offset_end);
+
+            // Now set the effective time/data range and duration/size for the time range
+            offsets_for_time_range (index_path, is_reverse,
+                                   ref time_offset_start, ref time_offset_end,
+                                   total_duration,
+                                   out this.range_start, out this.range_end,
+                                   total_size );
+
+            if (this.content_protected) {
+                // We don't currently support Range on link-protected binaries. So leave out
+                //  the byte range from the TimeSeekRange response
+                var seek_response
+                    = new HTTPTimeSeekResponse.time_only (time_offset_start,
+                                                          time_offset_end,
+                                                          total_duration );
+                debug ("Time range for time seek: %lldms through %lldms",
+                         seek_response.start_time, seek_response.end_time);
+                response_list.add (seek_response);
+                perform_cleartext_response = true; // We'll packet-align the range below
+            } else { // No link protection
+                var seek_response = new HTTPTimeSeekResponse
+                                            (time_offset_start, time_offset_end,
+                                             total_duration,
+                                             this.range_start, this.range_end-1,
+                                             total_size);
+                debug ("Time range for time seek response: %lldms through %lldms",
+                       seek_response.start_time, seek_response.end_time);
+                debug ("Byte range for time seek response: bytes %lld through %lld",
+                       seek_response.start_byte, seek_response.end_byte );
+                response_list.add (seek_response);
+                perform_cleartext_response = false;
+            }
+        } else if (seek_request is HTTPByteSeekRequest) {
+            //
+            // Byte-based seek (only for non-protected content currently)
+            //
+            if (this.content_protected) { // Sanity check
+                throw new DataSourceError.GENERAL
+                              ("Byte seek not supported on protected content");
+            }
+            var byte_seek = seek_request as HTTPByteSeekRequest;
+            debug ("Processing byte seek (bytes %lld to %s)", byte_seek.start_byte,
+                   (byte_seek.end_byte == HTTPSeekRequest.UNSPECIFIED)
+                   ? "*" : byte_seek.end_byte.to_string () );
+            this.range_start = byte_seek.start_byte;
+            if (byte_seek.end_byte == HTTPSeekRequest.UNSPECIFIED) {
+                this.range_end = total_size;
+            } else {
+                this.range_end = int64.min (byte_seek.end_byte + 1, total_size);
+            }
+            var seek_response = new HTTPByteSeekResponse (this.range_start,
+                                                          this.range_end-1,
+                                                          total_size);
+            debug ("Byte range for byte seek response: bytes %lld through %lld",
+                   seek_response.start_byte, seek_response.end_byte );
+            response_list.add (seek_response);
+            perform_cleartext_response = false;
+        } else if (seek_request is DTCPCleartextByteSeekRequest) {
+            //
+            // Cleartext-based seek (only for link-protected content)
+            //
+            if (!this.content_protected) { // Sanity check
+                throw new DataSourceError.GENERAL ("Cleartext seek not supported on unprotected content");
+            }
+            var cleartext_seek = seek_request as DTCPCleartextByteSeekRequest;
+            debug ( "Processing cleartext byte seek (bytes %lld to %s)",
+                      cleartext_seek.start_byte,
+                      (cleartext_seek.end_byte == HTTPSeekRequest.UNSPECIFIED)
+                      ? "*" : cleartext_seek.end_byte.to_string () );
+            this.range_start = cleartext_seek.start_byte;
+            if (cleartext_seek.end_byte == HTTPSeekRequest.UNSPECIFIED) {
+                this.range_end = total_size;
+            } else {
+                this.range_end = int64.min (cleartext_seek.end_byte + 1, total_size);
+            }
+            perform_cleartext_response = true; // We'll packet-align the range below
+        } else {
+            throw new DataSourceError.SEEK_FAILED ("Unsupported seek type");
+        }
+
+        if (perform_cleartext_response) {
+            // The range needs to be packet-aligned, which can affect range returned
+            get_packet_aligned_range ( res, this.range_start, this.range_end,
+                                      total_size,
+                                      out this.range_start, out this.range_end );
+            var seek_response
+                = new DTCPCleartextByteSeekResponse (this.range_start,
+                                                     this.range_end-1,
+                                                     total_size);
+
+            seek_response.encrypted_length = (int64)
+                                             Dtcpip.get_encrypted_length
+                                                 (seek_response.range_length,
+                                                  ODIDMediaEngine.chunk_size);
+            response_list.add (seek_response);
+            debug ("Byte range for cleartext byte seek response: bytes %lld through %lld",
+                   seek_response.start_byte, seek_response.end_byte );
+        }
+
+        return response_list;
+        // Wait for a start() before sending anything
+    }
+
+    /**
+     * Note: range_end and aligned_end are non-inclusive
+     */
+    internal void get_packet_aligned_range (MediaResource res,
+                                            int64 range_start, int64 range_end,
+                                            int64 total_size,
+                                            out int64 aligned_start,
+                                            out int64 aligned_end) {
+        //Get the transport stream packet size for the profile
+        string profile_name = res.dlna_profile;
+        // Align the bytes to transport packet boundaries
+        int64 packet_size = ODIDUtil.get_profile_packet_size (profile_name);
+        aligned_start = range_start;
+        if (packet_size > 0) {
+            // DLNA Link Protection : 8.9.5.4.2
+            aligned_end = ODIDUtil.get_dtcp_aligned_end (range_start, range_end,
+                                                         packet_size);
+            aligned_end = int64.min (aligned_end, total_size);
+        } else {
+            warning ("Attemped to DTCP-align unsupported protocol: "
+                     + profile_name);
+            aligned_end = range_end;
+        }
+    }
+
+    /**
+     * Find the time/data offsets that cover the provided time range start_time to end_time.
+     *
+     * Note: This method will clamp the end time/offset to the duration/total_size if not
+     *       present in the index file.
+     */
+    internal void offsets_for_time_range (string index_path, bool is_reverse,
+                                          ref int64 start_time, ref int64 end_time,
+                                          int64 total_duration,
+                                          out int64 start_offset, out int64 end_offset,
+                                          int64 total_size)
+         throws Error {
+        debug ("offsets_for_time_range: %s, %lld-%s",
+               index_path, start_time,
+               ((end_time != int64.MAX) ? end_time.to_string () : "*") );
+        bool start_offset_found = false;
+        bool end_offset_found = false;
+
+        var file = File.new_for_uri (index_path);
+        var dis = new DataInputStream (file.read ());
+        string line;
+        int64 cur_time_offset = 0;
+        string cur_data_offset = null;
+        int64 last_time_offset = is_reverse ? total_duration : 0; // Time can go backward
+        string last_data_offset = "0"; // but data offsets always go forward...
+        start_offset = int64.MAX;
+        end_offset = int64.MAX;
+        int line_count = 0;
+        // Read lines until end of file (null) is reached
+        while ((line = dis.read_line (null)) != null) {
+            line_count++;
+            // Entry Type (V: Video Frame)
+            // | Video Frame Type (I,B,P)
+            // | | Time Offset (seconds.milliseconds) (fixed decimal places, 8.3)
+            // | | |            File Byte Offset (fixed decimal places, 19)
+            // | | |            |                   Frame size (fixed decimal places, 10)
+            // | | |            |                   |
+            // v v v            v                   v
+            // V F 00000000.000 0000000000000000000 0000000000<16 spaces><newline>
+            if (line.length != ODIDMediaEngine.INDEXFILE_ROW_SIZE-1) {
+                throw new ODIDMediaEngineError.INDEX_FILE_ERROR (
+                              "Bad index file entry size (line %d of %s is %d bytes - should be %d bytes): '%s'",
+                              line_count, index_path, line.length,
+                              ODIDMediaEngine.INDEXFILE_ROW_SIZE, line);
+            }
+            var index_fields = line.split (" "); // Could use fixed field positions here...
+            if ((index_fields[0][0] == 'V') && (index_fields[1][0] == 'I')) {
+                string time_offset_string = index_fields[2];
+                string extended_time_string = time_offset_string[0:7]
+                                              + time_offset_string[8:11]
+                                              + "000";
+                // Leading "0"s cause parse() to assume the value is octal (see Vala bug 656691)
+                cur_time_offset = int64.parse (strip_leading_zeros
+                                                   (extended_time_string));
+                cur_data_offset = index_fields[3]; // Convert this only when needed
+                // debug ("offsets_for_time_range: keyframe at %s (%s) has offset %s",
+                //       extended_time_string, cur_time_offset.to_string(), cur_data_offset);
+                if (!start_offset_found) {
+                    if ( (is_reverse && (cur_time_offset < start_time))
+                         || (!is_reverse && (cur_time_offset > start_time)) ) {
+                        start_time = last_time_offset;
+                        start_offset = int64.parse (strip_leading_zeros
+                                                        (last_data_offset));
+                        start_offset_found = true;
+                        debug ("offsets_for_time_range: found start of range (%s): time %lld, offset %lld",
+                               (is_reverse ? "reverse" : "forward"),
+                               start_time, start_offset);
+                    }
+                } else {
+                    if ( (is_reverse && (cur_time_offset < end_time))
+                         || (!is_reverse && (cur_time_offset > end_time)) ) {
+                        end_time = cur_time_offset;
+                        end_offset = int64.parse (strip_leading_zeros
+                                                      (cur_data_offset));
+                        end_offset_found = true;
+                        debug ("offsets_for_time_range: found end of range (%s): time %lld, offset %lld",
+                               (is_reverse ? "reverse" : "forward"),
+                               end_time, end_offset);
+                        break;
+                    }
+                }
+                last_time_offset = cur_time_offset;
+                last_data_offset = cur_data_offset;
+            }
+        }
+
+        if (!start_offset_found) {
+            throw new DataSourceError.SEEK_FAILED ("Start time %lld is out of index file range",
+                                                  start_time);
+        }
+
+        if (!end_offset_found) {
+            // Modify the end byte value to align to start/end of the file, if necessary
+            //  (see DLNA 7.5.4.3.2.24.4)
+            end_offset = total_size;
+            if (is_reverse) {
+                end_time = 0;
+            } else {
+                end_time = total_duration;
+            }
+            debug ("offsets_for_time_range: end of range beyond index range (%s): time %lld, offset %lld",
+                   (is_reverse ? "reverse" : "forward"), end_time, end_offset);
+        }
+    }
+
+    internal static string strip_leading_zeros (string number_string) {
+        return ODIDMediaEngine.strip_leading_zeros (number_string);
+    }
+
+    internal string ? get_resource_property (string odid_resource_uri,
+                                             string property_name)
+         throws Error {
+        return get_property_from_file (odid_resource_uri + "resource.info",
+                                       property_name);
+    }
+
+    internal string ? get_content_property (string odid_content_uri,
+                                            string property_name)
+         throws Error {
+        return get_property_from_file (odid_content_uri + ".info", property_name);
+    }
+
+    internal string ? get_property_from_file (string uri, string property_name)
+         throws Error {
+        var file = File.new_for_uri (uri);
+        var dis = new DataInputStream (file.read ());
+        string line;
+        // Read lines until end of file (null) is reached
+        while ((line = dis.read_line (null)) != null) {
+            if (line[0] == '#') continue;
+            var equals_pos = line.index_of ("=");
+            var name = line[0:equals_pos].strip ();
+            if (name == property_name) {
+                var value = line[equals_pos+1:line.length].strip ();
+                return ((value.length == 0) ? null : value);
+            }
+        }
+
+        return null;
+    }
+
+    public void start () throws Error {
+        debug ("Starting data source for %s", content_uri);
+
+        if (this.content_protected) {
+            Dtcpip.server_dtcp_open (out dtcp_session_handle, 0);
+
+            if (dtcp_session_handle == -1) {
+                warning ("DTCP-IP session not opened");
+                throw new DataSourceError.GENERAL ("Error starting DTCP session");
+            } else {
+                debug ("Entering DTCP-IP streaming mode (session handle 0x%X)",
+                         dtcp_session_handle);
+            }
+        }
+
+        // TODO: Change this to use a persistent thread or thread pool...
+        this.thread = new Thread<void*> ("ODIDDataSource Serving thread",
+                                         this.thread_func);
+    }
+
+    public void freeze () {
+        if (this.frozen) {
+            return;
+        }
+
+        this.mutex.lock ();
+        this.frozen = true;
+        this.mutex.unlock ();
+    }
+
+    public void thaw () {
+        if (!this.frozen) {
+            return;
+        }
+
+        this.mutex.lock ();
+        this.frozen = false;
+        this.cond.broadcast ();
+        this.mutex.unlock ();
+    }
+
+    public void stop ()
+    {
+        if (this.stop_thread)
+        {
+            return;
+        }
+
+        this.mutex.lock ();
+        this.frozen = false;
+        this.stop_thread = true;
+        this.cond.broadcast ();
+        this.mutex.unlock ();
+    }
+
+    public void clear_dtcp_session () {
+        if (dtcp_session_handle != -1) {
+            int ret_close = Dtcpip.server_dtcp_close (dtcp_session_handle);
+            debug ("Dtcp session closed : %d",ret_close);
+            dtcp_session_handle = -1;
+        }
+    }
+    private void* thread_func () {
+        var file = File.new_for_uri (this.content_uri);
+        debug ("Spawned new thread for streaming %s", this.content_uri);
+        try {
+            var mapped = new MappedFile (file.get_path (), false);
+
+            if (this.range_end == 0) {
+                this.range_end = mapped.get_length ();
+            }
+
+            debug ("Sending bytes %lld-%lld (%lld bytes) of %s",
+                   this.range_start, this.range_end, this.range_end-this.range_start,
+                   this.content_uri );
+
+            while (true) {
+                bool exit;
+                this.mutex.lock ();
+                while (this.frozen) {
+                    this.cond.wait (this.mutex);
+                }
+
+                exit = this.stop_thread;
+                this.mutex.unlock ();
+
+                if (exit || this.range_start >= this.range_end) {
+                    debug ("Done streaming!");
+                    break;
+                }
+
+                var start = this.range_start;
+                var stop = start + ODIDMediaEngine.chunk_size;
+                if (stop > this.range_end) {
+                    stop = this.range_end;
+                }
+
+                // debug ( "Sending range %lld-%lld (%lld bytes)",
+                //           start, stop, stop-start );
+
+                unowned uint8[] data = (uint8[]) mapped.get_contents ();
+                data.length = (int) mapped.get_length ();
+                uint8[] slice = data[start:stop];
+                this.range_start = stop; // Move forward
+
+                if (this.dtcp_session_handle == -1) {
+                    // There's a potential race condition here.
+                    Idle.add ( () => {
+                        if (!this.stop_thread) {
+                            this.data_available (slice);
+                        }
+                        return false;
+                    });
+                } else {
+                    // Encrypted data has to be unowned as the reference will be passed
+                    // to DTCP libraries to perform the cleanup, else vala will be
+                    //performing the cleanup by default.
+                    unowned uint8[] encrypted_data = null;
+                    uchar cci = 0x3; // TODO: Put the CCI bits in resource.info
+
+                    // Encrypt the data
+                    int return_value = Dtcpip.server_dtcp_encrypt
+                                           ( dtcp_session_handle, cci,
+                                             slice, out encrypted_data );
+                    debug ("Encryption returned: %d and the encryption size : %s",
+                           return_value,
+                           (encrypted_data == null)
+                            ? "NULL" : encrypted_data.length.to_string ());
+                    // There's a potential race condition here.
+                    Idle.add ( () => {
+                        if (!this.stop_thread) {
+                            this.data_available (encrypted_data);
+                        }
+
+                        int ret_free = Dtcpip.server_dtcp_free (encrypted_data);
+                        debug ("DTCP-IP data reference freed : %d", ret_free);
+
+                        return false;
+                    });
+                }
+            }
+        } catch (Error error) {
+            warning ("Failed to stream: %s", error.message);
+        }
+
+        // Signal that we're done streaming
+        Idle.add ( () => { this.done (); return false; });
+
+        return null;
+    }
+}
diff --git a/src/media-engines/odid/odid-media-engine.vala b/src/media-engines/odid/odid-media-engine.vala
new file mode 100644
index 0000000..5802235
--- /dev/null
+++ b/src/media-engines/odid/odid-media-engine.vala
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ * Contact: http://www.cablelabs.com/
+ *
+ * Author: Craig Pratt <craig@ecaspia.com>
+ *         Parthiban Balasubramanian <P.Balasubramanian-contractor@cablelabs.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+using Gee;
+using GUPnP;
+using Dtcpip;
+
+public errordomain Rygel.ODIDMediaEngineError {
+    CONFIG_ERROR,
+    INDEX_FILE_ERROR
+}
+
+/**
+ * This media engine is intended to be the basis for the CL
+ * reference DMS. Long-term, this could be moved outside the Rygel
+ * source tree and built stand-alone.
+ */
+internal class Rygel.ODIDMediaEngine : MediaEngine {
+    private  GLib.List<DLNAProfile> profiles
+        = new GLib.List<DLNAProfile> ();
+
+    // Entry Type (V: Video Frame)
+    // | Video Frame Type (I,B,P)
+    // | | Time Offset (seconds.milliseconds) (fixed decimal places, 8.3)
+    // | | |           File Byte Offset (fixed decimal places, 19)
+    // | | |           |                   Frame size (fixed decimal places, 10)
+    // | | |           |                   |
+    // v v v           v                   v
+    // V F 0000000.000 0000000000000000000 0000000000<15 spaces><newline>
+
+    public static const uint INDEXFILE_ROW_SIZE = 62;
+    public static const uint INDEXFILE_FIELD_ENTRYTYPE_OFFSET = 0;
+    public static const uint INDEXFILE_FIELD_FRAMETYPE_OFFSET = 2;
+    public static const uint INDEXFILE_FIELD_TIME_OFFSET = 4;
+    public static const uint INDEXFILE_FIELD_TIMESECONDS_LENGTH = 7;
+    public static const uint INDEXFILE_FIELD_TIMEMS_LENGTH = 3;
+    public static const uint INDEXFILE_FIELD_TIME_LENGTH = 11;
+    public static const uint INDEXFILE_FIELD_BYTEOFFSET_OFFSET = 17;
+    public static const uint INDEXFILE_FIELD_BYTEOFFSET_LENGTH = 19;
+    public static const uint INDEXFILE_FIELD_FRAMESIZE_OFFSET = 37;
+    public static const uint INDEXFILE_FIELD_FRAMESIZE_LENGTH = 10;
+
+    // Variable to hold chunk size for streaming
+    public static int64 chunk_size;
+
+    // DTCP control variables
+    private bool dtcp_initialized;
+    private string dtcp_storage;
+    private ushort dtcp_port; // Default DTCP port is 8999
+    private string dtcp_host;
+
+    public ODIDMediaEngine () {
+        debug ("constructing");
+        var profiles_config = new Gee.ArrayList<string> ();
+        var config = MetaConfig.get_default ();
+
+        bool dtcp_enabled = false;
+        string chunk_size_str;
+        try {
+            dtcp_enabled = config.get_bool ("OdidMediaEngine", "dtcp-enabled");
+            profiles_config = config.get_string_list ("OdidMediaEngine", "profiles");
+            chunk_size_str = config.get_string ("OdidMediaEngine", "chunk-size");
+        } catch (Error err) {
+            error ("Error reading ODIDMediaEngine property: " + err.message);
+        }
+
+        chunk_size = ODIDUtil.get_chunk_size (chunk_size_str);
+
+        dtcp_initialized = false;
+        if (dtcp_enabled) {
+            try {
+                this.dtcp_storage = config.get_string ("OdidMediaEngine", "dtcp-storage");
+                this.dtcp_host = config.get_string ("OdidMediaEngine", "dtcp-host");
+                this.dtcp_port = (ushort)config.get_int ("OdidMediaEngine", "dtcp-port",
+                                                         6000, 8999);
+                if (Dtcpip.init_dtcp_library (dtcp_storage) != 0) {
+                    error ("DTCP-IP init failed for storage path: %s",dtcp_storage);
+                } else {
+                    debug ("DTCP-IP storage loaded successfully");
+                    if (Dtcpip.server_dtcp_init (dtcp_port) != 0) {
+                        error ("DTCP-IP source init failed: host %s, port %d, storage %s",
+                               this.dtcp_host, this.dtcp_port, this.dtcp_storage);
+                    } else {
+                        debug ("DTCP-IP source initialized: host %s, port %d, storage %s",
+                               this.dtcp_host, this.dtcp_port, this.dtcp_storage);
+                        dtcp_initialized = true;
+                    }
+                }
+            } catch (Error err) {
+                error ("Error initializing DTCP: " + err.message);
+            }
+        } else {
+            debug ("DTCP-IP is disabled");
+        }
+
+        foreach (var row in profiles_config) {
+            var columns = row.split (",");
+            if (columns.length < 2) {
+                debug ("OdidMediaEngine profile entry \""
+                       + row + "\" is malformed: Expected 2 entries and found "
+                       + columns.length.to_string () );
+                break;
+            }
+
+            debug ("OdidMediaEngine: configuring profile entry: " + row);
+            // Note: This profile list won't affect what profiles are included in the
+            //       primary res block
+            profiles.append (new DLNAProfile (columns[0],columns[1]));
+        }
+    }
+
+    public override unowned GLib.List<DLNAProfile> get_dlna_profiles () {
+        debug ("get_dlna_profiles");
+        return this.profiles;
+    }
+
+    public override async Gee.List<MediaResource> ? get_resources_for_item
+                                                        (MediaObject object) {
+        // TODO: Change this to "object is OdidMediaItem"
+        if (! (object is MediaItem)) {
+            warning ("Can only process odid MediaObjects (OdidMediaItems)");
+            return null;
+        }
+
+        var item = object as MediaItem;
+
+        // For MediaFileItems, uri 0 is the file URI referring directly to
+        //  the content. But for us, we're presuming it refers to the ODID base
+        //  directory (this becomes less squishy when OdidMediaItem comes on the scene...)
+        string source_uri = item.uris.get (0);
+
+        // TODO: Consider our own uri scheme (e.g. "odid:")
+        if (!source_uri.has_prefix ("file://")) {
+            warning ("Can't process non-file uri " + source_uri);
+        }
+
+        debug ("OdidMediaEngine:get_resources: " + source_uri);
+
+        Gee.List<MediaResource> resources = new Gee.ArrayList<MediaResource> ();
+
+        string odid_item_path = null;
+        try {
+            File odidItem = File.new_for_uri (source_uri);
+            KeyFile keyFile = new KeyFile ();
+            keyFile.load_from_file (odidItem.get_path (),
+                                    KeyFileFlags.KEEP_COMMENTS |
+                                    KeyFileFlags.KEEP_TRANSLATIONS);
+
+            if (keyFile.has_key ("item", "odid_uri"))    {
+                odid_item_path = keyFile.get_string ("item", "odid_uri");
+            } else {
+                if (odidItem.get_parent () != null) {
+                    odid_item_path = odidItem.get_parent ().get_uri () + "/";
+                } else {
+                    warning ("ODID MediaObject does not have top level directory");
+                    return null;
+                }
+            }
+
+            debug ("get_resources: processing item directory: " + odid_item_path);
+
+            var directory = File.new_for_uri (odid_item_path);
+
+            var enumerator = directory.enumerate_children
+                                           (FileAttribute.STANDARD_NAME, 0);
+
+            FileInfo file_info;
+            while ((file_info = enumerator.next_file ()) != null) {
+                if (file_info.get_file_type () == FileType.DIRECTORY) {
+                    debug ("get_resources:   processing resource directory: "
+                           + file_info.get_name ());
+                    // A directory in an item directory is a resource
+                    try {
+                        var res = create_resource_from_resource_dir
+                                      (odid_item_path + file_info.get_name () + "/");
+                        if (res != null) {
+                            resources.add (res);
+                            debug ("get_resources:     created resource: "
+                                   + res.to_string ());
+                        }
+                    } catch (Error err) {
+                        warning ("Error processing item resource %s: %s",
+                                 odid_item_path + file_info.get_name (), err.message);
+                        // Continue processing other resources
+                    }
+                }
+            }
+        } catch (Error err) {
+            warning ("Error creating resources for source %s: %s",
+                     odid_item_path, err.message);
+        }
+
+        return resources;
+    }
+
+    /**
+     * Construct a MediaResource from an on-disk resource
+     *
+     * @param res_dir_uri URI to the resource directory.
+     * @return MediaResource constructed from the on-disk resource directory
+     */
+    internal MediaResource ? create_resource_from_resource_dir (string res_dir_uri)
+         throws Error {
+        debug ( "OdidMediaEngine:create_resource_from_resource_dir: configuring resources for "
+                 + res_dir_uri);
+        MediaResource res = null;
+
+        var res_dir = File.new_for_uri (res_dir_uri);
+        FileInfo res_dir_info = res_dir.query_info
+                                            (GLib.FileAttribute.STANDARD_NAME, 0);
+
+        res = new MediaResource (res_dir_info.get_name ());
+        // Assert: All res values are set to sane/unset defaults
+
+        string basename = null;
+        bool dtcp_protected = false;
+        bool is_converted = false;
+
+        // Process fields set in the resource.info
+        {
+            File res_info_file = File.new_for_uri (res_dir_uri + "/resource.info");
+            var dis = new DataInputStream (res_info_file.read ());
+            string line;
+            int line_num = 0;
+            while ((line = dis.read_line (null)) != null) {
+                line_num++;
+                if (line[0] == '#') continue;
+                var equals_pos = line.index_of ("=");
+                var name = line[0:equals_pos].strip ();
+                var value = line[equals_pos+1:line.length].strip ();
+
+                if ((name == null) || (value == null))  {
+                    warning ("Bad entry in %s line %d: %s",
+                             res_dir_uri, line_num, line);
+                    continue;
+                }
+                if (name == "basename") {
+                    basename = value;
+                    continue;
+                }
+                if (name == "protected") {
+                    dtcp_protected = (value == "true") && dtcp_initialized;
+                    continue;
+                }
+                if (name == "converted") {
+                    is_converted = (value == "true");
+                    continue;
+                }
+                if (name.length > 0 && value.length > 0) {
+                    set_resource_field (res, name, value);
+                }
+            }
+        }
+
+        string file_extension;
+        string normal_content_filename;
+
+        // Modify the profile & mime type for DTCP, if necessary
+        if (dtcp_protected) {
+            res.mime_type = dtcp_mime_type_for_mime_type (res.mime_type);
+            res.dlna_profile = "DTCP_" + res.dlna_profile;
+        }
+
+        if (res.uri != null) {
+            // Our URI (and content) is not Rygel-hosted. So no need to look for content
+            //  (there really shouldn't be any...)
+            debug ("Found URI in resource metadata for %s: %s", res_dir_uri, res.uri);
+            return res;
+        }
+
+        // Check for required properties
+        if (basename == null) {
+            throw new ODIDMediaEngineError.CONFIG_ERROR
+                          ("No basename property set for resource");
+        }
+
+        // Set the size according to the normal-rate file (speed "1/1")
+        {
+            normal_content_filename = ODIDMediaEngine.content_filename_for_res_speed
+                                                          (res_dir_uri, basename, null,
+                                                           out file_extension);
+            File content_file = File.new_for_uri (res_dir_uri
+                                                  + normal_content_filename);
+            FileInfo content_info = content_file.query_info
+                                        (GLib.FileAttribute.STANDARD_SIZE, 0);
+            res.size = content_info.get_size ();
+            res.extension = file_extension;
+            debug ( "create_resource_from_resource_dir: size for "
+                     + normal_content_filename + " is " + res.size.to_string () );
+        }
+
+        res.dlna_flags = DLNAFlags.DLNA_V15
+                         | DLNAFlags.STREAMING_TRANSFER_MODE
+                         | DLNAFlags.BACKGROUND_TRANSFER_MODE
+                         | DLNAFlags.CONNECTION_STALL;
+
+        res.dlna_conversion = is_converted ? DLNAConversion.TRANSCODED
+                                           : DLNAConversion.NONE;
+
+        // We currently support RANGE for all resources except DTCP content
+        if (dtcp_protected) {
+            res.dlna_flags |= DLNAFlags.LINK_PROTECTED_CONTENT
+                              | DLNAFlags.CLEARTEXT_BYTESEEK_FULL;
+            res.dlna_operation = DLNAOperation.NONE;
+            // We'll OR in TIMESEEK if we have an index file...
+            res.cleartext_size = res.size;
+            res.size = (int64)Dtcpip.get_encrypted_length (res.cleartext_size,
+                                                           chunk_size);
+            debug ("Encrypted size from DTCP library: %lld",res.size);
+        } else {
+            res.dlna_operation = DLNAOperation.RANGE;
+            // We'll OR in TIMESEEK if we have an index file...
+        }
+
+        // Look for an index file and set fields accordingly if/when found
+        {
+            string index_path = res_dir_uri + normal_content_filename + ".index";
+            File index_file = File.new_for_uri (index_path);
+            if (index_file.query_exists ()) {
+                // We support TimeSeekRange for this content
+                res.dlna_operation |= DLNAOperation.TIMESEEK;
+                // Set the duration according to the last entry in the normal-rate index file
+                res.duration = duration_from_index_file (index_file);
+                debug ( "create_resource_from_resource_dir: duration for "
+                       + normal_content_filename + " is "
+                       + res.duration.to_string () );
+            } else {
+                debug ( "create_resource_from_resource_dir: No index file found for "
+                       + res_dir_uri + normal_content_filename );
+            }
+        }
+
+        // Look for scaled files and set fields accordingly if/when found
+        {
+            Gee.List<PlaySpeed> playspeeds;
+
+            playspeeds = find_playspeeds_for_res (res_dir_uri, basename);
+
+            if (playspeeds != null) {
+                var speed_array = new string[playspeeds.size];
+                int speed_index = 0;
+                foreach (var speed in playspeeds) {
+                    speed_array[speed_index++] = speed.to_string ();
+                }
+                res.play_speeds = speed_array;
+                debug ( "create_resource_from_resource_dir: Found %d speeds for "
+                       + res_dir_uri + normal_content_filename, speed_index);
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Return DTCP mime type string for a given mime type
+     */
+    public string dtcp_mime_type_for_mime_type (string mime_type) {
+        return "application/x-dtcp1;DTCP1HOST=" + this.dtcp_host.to_string ()
+                                + ";DTCP1PORT=" + this.dtcp_port.to_string ()
+                                + ";CONTENTFORMAT=\"" + mime_type + "\"";
+    }
+
+    /**
+     * Set a MediaResource field from a resource.info name-value pair
+     *
+     * @param res MediaResource to modify
+     * @param name The field name
+     * @param value The field value
+     */
+    void set_resource_field (MediaResource res, string name, string value) throws Error {
+        switch (name) {
+            case "profile":
+                res.dlna_profile = value;
+                break;
+            case "mime-type":
+                res.mime_type = value;
+                break;
+            case "bitrate":
+                res.bitrate = int.parse (value);
+                if (res.bitrate <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Invalid denominator: " + value);
+                }
+                break;
+            case "audio-bits-per-sample":
+                res.bits_per_sample = int.parse (value);
+                if (res.bits_per_sample <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource audio-bits-per-sample value: "
+                                   + value);
+                }
+                break;
+            case "video-color-depth":
+                res.color_depth = int.parse (value);
+                if (res.color_depth <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource video-color-depth value: "
+                                  + value);
+                }
+                break;
+            case "video-resolution":
+                var res_fields = value.split ("x");
+                if (res_fields.length != 2) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource video-resolution value: "
+                                   + value);
+                }
+                res.width = int.parse (res_fields[0]);
+                if (res.width <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource video-resolution x value: "
+                                   + value);
+                }
+                res.height = int.parse (res_fields[1]);
+                if (res.height <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource video-resolution y value: "
+                                   + value);
+                }
+                break;
+            case "audio-channels":
+                res.audio_channels = int.parse (value);
+                if (res.audio_channels <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource audio-channels value: "
+                                   + value);
+                }
+                break;
+            case "audio-sample-frequency":
+                res.sample_freq = int.parse (value);
+                if (res.sample_freq <= 0) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource audio-sample-frequency value: "
+                                   + value);
+                }
+                break;
+            // Note: The entries below here are intended for remotely-hosted content (e.g. MPEG-DASH)
+            case "uri":
+                res.uri = value;
+                break;
+            case "size":
+                int64 size;
+                bool parsed = int64.try_parse (value, out size);
+                if (parsed) {
+                    res.size = size;
+                } else {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad odid resource size value: " + value);
+                }
+                break;
+            case "duration":
+                int64 duration;
+                bool parsed = int64.try_parse (value, out duration);
+                if (parsed) {
+                    res.duration = (long)duration;
+                } else {
+                   throw new ODIDMediaEngineError.CONFIG_ERROR
+                                 ("Bad odid resource duration value: " + value);
+                }
+                break;
+            default:
+                throw new ODIDMediaEngineError.CONFIG_ERROR
+                              ("Unknown odid configuration entry: " + name);
+
+        } // END switch (name)
+    }
+
+    internal static string? content_filename_for_res_speed
+                                (string resource_dir_path,
+                                 string basename,
+                                 PlaySpeed? playspeed,
+                                 out string extension)
+            throws Error {
+        debug ("content_filename_for_res_speed: %s, %s, %s",
+               resource_dir_path,basename,
+               (playspeed != null) ? playspeed.to_string () : "null" );
+        string rate_string;
+        if (playspeed == null) {
+            rate_string = "1_1";
+        } else {
+            rate_string = playspeed.numerator.to_string ()
+                          + "_" + playspeed.denominator.to_string ();
+        }
+
+        string content_filename = null;
+        extension = null;
+
+        var directory = File.new_for_uri (resource_dir_path);
+        var enumerator = directory.enumerate_children
+                                       (GLib.FileAttribute.STANDARD_NAME, 0);
+
+        FileInfo file_info;
+        while ((file_info = enumerator.next_file ()) != null) {
+            var cur_filename = file_info.get_name ();
+            // Check for content file for the requested rate (<basename>.<rate>.<extension>)
+            var split_name = cur_filename.split (".");
+            if ( (split_name.length == 3)
+                 && (split_name[0] == basename)
+                 && (split_name[1] == rate_string) ) {
+                content_filename = cur_filename;
+                extension = split_name[2];
+                debug ("content_filename_for_res_speed: FOUND MATCH: %s (extension %s)",
+                       content_filename, extension);
+            }
+        }
+
+        return content_filename;
+    }
+
+    /**
+     * Produce a list of PlaySpeeds corresponding to scaled content files for the given
+     * resource directory and basename.
+     *
+     * @return A List with one PlaySpeed per scaled-rate content file
+     */
+    internal static Gee.List<PlaySpeed>? find_playspeeds_for_res ( string resource_dir_uri,
+                                                                      string basename )
+        throws Error {
+        debug ("ODIDMediaEngine.find_playspeeds_for_res: %s, %s",
+               resource_dir_uri,basename );
+        var speeds = new Gee.ArrayList<PlaySpeed> ();
+
+        var directory = File.new_for_uri (resource_dir_uri);
+        var enumerator = directory.enumerate_children (GLib.FileAttribute.STANDARD_NAME, 0);
+
+        FileInfo file_info;
+        while ((file_info = enumerator.next_file ()) != null) {
+            var cur_filename = file_info.get_name ();
+            // Only look for content files (<basename>.<rate>.<extension>)
+            var split_name = cur_filename.split (".");
+            if ((split_name.length == 3) && (split_name[0] == basename)) {
+                var speed_parts = split_name[1].split ("_");
+                if (speed_parts.length != 2) {
+                    throw new ODIDMediaEngineError.CONFIG_ERROR
+                                  ("Bad  speed found in res filename %s (%s)",
+                                   cur_filename, split_name[1]);
+                }
+                var speed = new PlaySpeed (int.parse (speed_parts[0]),int.parse (speed_parts[1]));
+                if (speed.numerator == 1 && speed.denominator == 1) {
+                    continue; // Rate "1" is implied and not included in the playspeeds - skip it
+                }
+                speeds.add (speed);
+            }
+        }
+        return (speeds.size > 0) ? speeds : null;
+    }
+
+    internal static long duration_from_index_file (File index_file)
+            throws Error {
+        debug ("ODIDDataSource.duration_for_content_file: %s",
+                 index_file.get_basename () );
+
+        FileInfo index_info = index_file.query_info
+                                             (GLib.FileAttribute.STANDARD_SIZE, 0);
+        var dis = new DataInputStream (index_file.read ());
+
+        // We don't need to parse the whole file.
+        // The last index entry will be INDEXFILE_ROW_SIZE bytes from the end of the file...
+
+        size_t last_entry_offset = (size_t)(index_info.get_size ()-INDEXFILE_ROW_SIZE);
+        dis.skip (last_entry_offset);
+        string line = dis.read_line (null);
+
+        if (line.length != ODIDMediaEngine.INDEXFILE_ROW_SIZE-1) {
+            throw new ODIDMediaEngineError.INDEX_FILE_ERROR
+                          ("Bad index file entry size (entry at offset %s of %s is %d bytes - should be %d bytes): '%s'",
+                           last_entry_offset.to_string (), index_file.get_basename (),
+                           line.length, ODIDMediaEngine.INDEXFILE_ROW_SIZE, line);
+        }
+        uint time_field_start = INDEXFILE_FIELD_TIME_OFFSET;
+        uint time_field_end = INDEXFILE_FIELD_TIME_OFFSET+INDEXFILE_FIELD_TIME_LENGTH;
+        string seconds_field = line[time_field_start:time_field_end];
+        double duration_val;
+        if (double.try_parse (seconds_field, out duration_val))
+        {
+            return (long)(duration_val+0.99); // For rounding
+        } else {
+            throw new ODIDMediaEngineError.INDEX_FILE_ERROR
+                        ("Misformed duration field ('%s') in last index file entry of %s: '%s'",
+                         seconds_field, index_file.get_basename (), line);
+        }
+    }
+
+    internal static string strip_leading_zeros (string number_string) {
+        int i=0;
+        while ((number_string[i] == '0') && (i < number_string.length)) {
+            i++;
+        }
+        if (i == 0) {
+            return number_string;
+        } else {
+            return number_string[i:number_string.length];
+        }
+    }
+
+    public override DataSource? create_data_source_for_resource
+                                    (MediaObject object, MediaResource resource) {
+        // TODO: Change this to "object is OdidMediaItem"
+        if (! (object is MediaItem)) {
+            warning ("Can only process odid MediaObjects (OdidMediaItems)");
+            return null;
+        }
+
+        var item = object as MediaItem;
+
+        // For MediaItems, uri 0 is the file URI referring directly to the content. But for
+        //  us, we're presuming it refers to the ODID base directory (this becomes less squishy
+        //  when OdidMediaItem comes on the scene...)
+        string source_uri = item.uris.get (0);
+
+        debug ("create_data_source_for_resource: source %s, resource %s",
+              source_uri, resource.get_name ());
+
+        // TODO: Consider our own uri scheme (e.g. "odid:")
+        if (!source_uri.has_prefix ("file://")) {
+            warning ("create_data_source_for_resource: can't process non-file uri "
+                     + source_uri);
+            return null;
+        }
+
+        debug ("create_data_source_for_resource: %s", resource.to_string ());
+
+        return new ODIDDataSource (source_uri, resource);
+    }
+
+    public override DataSource? create_data_source_for_uri (string source_uri) {
+        warning ("create_data_source_for_uri not supported");
+        return null;
+    }
+}
+
+public static Rygel.MediaEngine module_get_instance () {
+    debug ("module_get_instance");
+    return new Rygel.ODIDMediaEngine ();
+}
+
diff --git a/src/media-engines/odid/odid-util.vala b/src/media-engines/odid/odid-util.vala
new file mode 100644
index 0000000..24abe30
--- /dev/null
+++ b/src/media-engines/odid/odid-util.vala
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013  Cable Television Laboratories, Inc.
+ *
+ * Author: Parthiban Balasubramanian <P.Balasubramanian-contractor@cablelabs.com>
+ *         Craig Pratt <craig@ecaspia.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CABLE TELEVISION LABORATORIES
+ * INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+public class Rygel.ODIDUtil : Object {
+    private static ODIDUtil util = null;
+    public const int64 PACKET_SIZE_188 = 188;
+    public const int64 PACKET_SIZE_192 = 192;
+
+    public const int64 KILOBYTES_TO_BYTES = 1024;
+
+    private ODIDUtil () {
+    }
+
+    public static ODIDUtil get_default () {
+        if (util == null) {
+            util = new ODIDUtil ();
+        }
+
+        return util;
+    }
+
+    // DTCP content has to be aligned to a packet boundary.  188 or 192
+    // Note: end_byte is not inclusive
+    public static int64 get_dtcp_aligned_end (int64 start_byte, int64 end_byte, int64 packet_size) {
+        int64 temp_end;
+        if (end_byte > 0) {
+            // Check if the total length falls between the packet size.
+            // Else add bytes to complete packet size.
+            int64 req_length = end_byte - start_byte;
+            int64 add_bytes = (packet_size - (req_length % packet_size)) % packet_size;
+            temp_end = end_byte + add_bytes;
+        } else {
+            temp_end = end_byte;
+        }
+        return temp_end;
+    }
+
+    // Identify the packet size from the profile
+    public static int64 get_profile_packet_size (string profile) {
+        // TODO : Need to consider mime types other than MPEG.
+        if ((profile.has_prefix ("DTCP_MPEG_TS") ||
+             profile.has_prefix ("DTCP_AVC_TS")) &&
+             profile.has_suffix ("_ISO")) {
+            return PACKET_SIZE_188;
+        }
+        // For Timestamped 192 byte packets for non ISO profiles
+        if ((profile.has_prefix ("DTCP_MPEG_TS") ||
+             profile.has_prefix ("DTCP_AVC_TS")) &&
+             !profile.has_suffix ("_ISO")){
+            return PACKET_SIZE_192;
+        }
+        //TODO : Handle MPEG_PS content alignment.(DLNA Link Protection 8.9.5.1.1)
+        return 0;
+    }
+
+    /**
+     * Returns if the content is protected
+     */
+    public bool is_item_protected (MediaItem item){
+        return false;
+    }
+
+    /**
+     * Returns the chunk size in bytes to be used while streaming.
+     * rygel.conf will provide value in KiloBytes
+     */
+    public static int64 get_chunk_size (string chunk_size_str) {
+        var chunk_size = int64.parse (chunk_size_str) * KILOBYTES_TO_BYTES;
+        debug ("Streaming chunk size : %"+int64.FORMAT, chunk_size);
+        return chunk_size;
+    }
+
+}
