def _to_xml(self):
    # Check that the object has valid attributes set and raise an exception if they are
    # None or an empty string.
    #
    # All objects require either a URL to be set, so the framework can automatically generate
    # unique identifiers (rating_key) and metadata URLs (key). If the URL is not provided,
    # developers must set the key and rating_key attributes manually and provide a valid
    # callback for full metadata objects.
    #
    empty = ('', None)

    # If the metadata object has a 'file' attribute, synthesize a MediaObject and PartObject for it.
    if hasattr(self, 'file') and self.file:
      # Only allow agents to do this.
      assert self._core.plugin_class == 'Agent'

      def check(attr):
        if getattr(self, attr, None):
          raise AttributeError("Conflict with 'file' attribute", attr)

      check('url')
      check('key')
      check('rating_key')

      if len(self.items):
        raise AttributeError("The 'file' attribute cannot be provided for objects with a media & part hierarchy.")

      self.add(self._sandbox.environment[MediaObject.__name__](
        parts=[self._sandbox.environment[PartObject.__name__](file=self.file)]
      ))
      self.file = None

      has_part_with_file = True

    else:
      has_part_with_file = len(self.items) and len(self.items[0].parts) and self.items[0].parts[0].file

    if self._template.require_key_and_rating_key:
      if ((hasattr(self, 'url') and self.url not in empty) or \
          (hasattr(self, 'key') and self.key not in empty and hasattr(self, 'rating_key') and self.rating_key not in empty)) == False and not \
          (Framework.code.context.flags.indirect in self._context.flags or self._context.request.uri.startswith('/:/plugins/')):

        if not has_part_with_file:
          raise Framework.exceptions.AttributeException('If no URL is provided, the key and rating_key attributes must be set.')


    # When sending an indirect response, check for an empty key attribute and try to set it.
    # Missing keys make the LGTV unhappy.
    if Framework.code.context.flags.indirect in self._context.flags and self.key in empty and len(self.items) > 0 and len(self.items[0].parts) > 0:
      self.key = self.items[0].parts[0].key


    # If no source icon is defined, use a hosted resource with the current sandbox's identifier.
    if self.source_icon == None and not (hasattr(self._template, 'suppress_source_icon') and self._template.suppress_source_icon):
      self.source_icon = self._core.runtime.hosted_resource_url('image', 'source', self._sandbox.identifier)

    
    el = Framework.modelling.objects.ModelInterfaceObject._to_xml(self)
    
    # If the URL attribute is defined, add some URL Service magic where required
    if hasattr(self, 'url') and self.url != None:
      url = self._attributes['url']
      
      lookup_key = self._core.services.lookup_url_for_media_url(url, syncable = Framework.code.context.flags.syncable in self._core.sandbox.context.flags)
      
      # If a key isn't set, synthesize one using the URL service lookup method
      if hasattr(self, 'key') and self.key == None:
        el.set('key', lookup_key)
        
      # If a ratingKey isn't set, use the URL
      if hasattr(self, 'rating_key') == False or (hasattr(self, 'rating_key') and self.rating_key == None):
        el.set('ratingKey', self.url)
      
      # If there are no child objects, and this class expects children, add them by calling the URL service
      if len(self._objects) == 0 and len(type(self)._child_types) > 0:
        
        # If the media objects function flagged as deferred, set the deferred attribute on the metadata object
        service = self._core.services.service_for_url(url)
        if self._core.services.function_in_service_is_deferred(Framework.components.services.MEDIA_OBJECTS_FUNCTION_NAME, service):
          self.deferred = True
          
        # If the item is deferred, add a synthetic item for older clients
        if self.deferred:
          el.set('deferred', '1')
          self._append_children(
            el,
            [self._sandbox.environment[MediaObject.__name__](
              parts = [
                self._sandbox.environment[PartObject.__name__](key = indirect_callback_string(lookup_key + "&indirect=1&limit=1"))
              ]
            )]
          )
          
        # If the item is not deferred, add its children normally
        else:
          items = self._core.services.media_objects_for_url(url, allow_deferred=True, metadata_class=type(self))
          if items:
            self._append_children(el, items)

    elif has_part_with_file:
      part = self.items[0].parts[0]
      file_url = urlparse.urljoin('file:', urllib.pathname2url(part.file))
      el.set('ratingKey', file_url)
          
          
    # If this object supports setting HTTP headers, it's a non-media object - apply HTTP headers and POST callback info
    if 'http_headers' in self._attribute_list:
      ObjectWithHTTPHeaders._apply_http_headers(self, el)
      ObjectWithPOSTCallback._apply_post_url_and_headers(self.key, el)
        
    return el
  def _to_xml(self):
    if len(self.parts) > 0:
      part = self.parts[0]

      if isinstance(part.key, WebkitURL):
        if self.protocol == None: self.protocol = 'webkit'
        
      elif isinstance(part.key, HLSURL):
        if self.protocol == None: self.protocol = 'hls'
        if self.audio_codec == None: self.audio_codec = 'aac'
        if self.video_codec == None: self.video_codec = 'h264'
        if self.container == None: self.container = 'mpegts'

      elif isinstance(part.key, RTMPURL):
        if self.protocol == None: self.protocol = 'rtmp'
      
      elif isinstance(part.key, EmbedURL):
        if self.protocol == None: self.protocol = 'embed'

    # Convert resolution to width/height if not already set.
    if self.video_resolution and self.width == None and self.height == None:
      height = 360 if self.video_resolution == 'sd' else int(self.video_resolution)
      aspect_ratio = float(self.aspect_ratio) if self.aspect_ratio != None and float(self.aspect_ratio) > 0 else float(16) / float(9)

      self.height = height
      self.width = int(8 * round((self.height * aspect_ratio)/8))

    # Copy old-style attributes to parts and create stream objects if none exist.
    for part in self.parts:
      if part.container == None and self.container != None: part.container = self.container
      if part.optimized_for_streaming == None and self.optimized_for_streaming != None: part.optimized_for_streaming = self.optimized_for_streaming
      if part.duration == None and self.duration != None: part.duration = self.duration

      if len(self.parts) == 1:
        if part.duration == None and self.duration != None: part.duration = self.duration
      else:
        self._warn("Media object has multiple parts - unable to synthesize duration.")

      if len(part.streams) == 0:
        self._warn("Media part has no streams - attempting to synthesize")

        bitrate = int(self.bitrate) if self.bitrate is not None else None

        if 'VideoStreamObject' in self._sandbox.environment:
          video_stream = self._sandbox.environment['VideoStreamObject'](
            width = self.width,
            height = self.height,
            codec = self.video_codec,
          )
          if len(self.parts) == 1:
            video_stream.duration = self.duration

          if bitrate > 256:
            video_stream.bitrate = bitrate - 256
          part.add(video_stream)

        if 'AudioStreamObject' in self._sandbox.environment:
          audio_stream = self._sandbox.environment['AudioStreamObject'](
            codec = self.audio_codec,
            duration = self.duration,
            channels = self.audio_channels,
          )
          if len(self.parts) == 1:
            audio_stream.duration = self.duration
          if bitrate > 256:
            audio_stream.bitrate = 256
          part.add(audio_stream)

          # Add media info to the URL for indirect callbacks.
          if isinstance(part.key, indirect_callback_string):
            media_info = dict(
              bitrate = self.bitrate,
              aspect_ratio = self.aspect_ratio,
              audio_channels = self.audio_channels,
              audio_codec = self.audio_codec,
              video_codec = self.video_codec,
              video_resolution = self.video_resolution,
              container = self.container,
              video_frame_rate = self.video_frame_rate,
              duration = self.duration,
              width = self.width,
              height = self.height,
              protocol = self.protocol,
              optimized_for_streaming = self.optimized_for_streaming
            )

            # Rewrite the key to include media info, making sure we copy POST properties.
            new_key = indirect_callback_string(part.key + ('&' if '?' in part.key else '?') + 'mediaInfo=' + urllib.quote(self._core.data.json.to_string(media_info)))

            new_key.post_url = part.key.post_url
            new_key.post_headers = part.key.post_headers

            part.key = new_key

      # If this part has stream info added, make sure that one of each type is flagged as selected.
      all_video_streams = [stream for stream in part.streams if isinstance(stream, VideoStreamObject)]
      all_audio_streams = [stream for stream in part.streams if isinstance(stream, AudioStreamObject)]

      for stream_list in (all_video_streams, all_audio_streams):
        if len(stream_list) > 0:
          selected_count = len([stream for stream in stream_list if stream.selected is True])
          if selected_count == 0:
            stream_list[0].selected = True
          elif selected_count > 1:
            self._core.log.warn("Part with key '%s' has multiple streams of the same type that are marked as selected.", part.key)


    el = Framework.modelling.objects.Container._to_xml(self)

    # If the first media part is an IndirectFunction instance, flag it as 'indirect'
    if isinstance(self._objects[0].key, indirect_callback_string):
      el.set('indirect', '1')
  
    return el