Treat multiple files as one

I’m building a channel to play videos. Some of these videos come in multiple parts, usually of filetype .ts or .mkv. How can I have PMS treat these files sequentially, so that it seamlessly plays one after the other as though they were one file?

Here is my _ _ init.py _ _ thus far:

PREFIX = '/video/pmatches'
ART = 'art-default.jpg'
ICON = 'icon-default.png'

RSS_FEED = 'http://<URL>/matches.rss.php'

#####################################################################
# This (optional) function is initially called by the PMS framework to
# initialize the plug-in. This includes setting up the Plug-in static
# instance along with the displayed artwork.

def Start(): # Initialize the plug-in
  # Setup the default attributes for the ObjectContainer
  ObjectContainer.title1 = TITLE	
  ObjectContainer.art = R(ART)

  # Setup the default attributes for the other objects
  DirectoryObject.thumb = R(ICON)
  DirectoryObject.art = R(ART)
  VideoClipObject.thumb = R(ICON)
  VideoClipObject.art = R(ART)

# Plugin's unique identifier
# Type: Video
# Name: soccermatches
@handler(PREFIX, TITLE)

def MainMenu():
  oc = ObjectContainer()
  
  for match in XML.ElementFromURL(RSS_FEED).xpath('//match'):
	competition = match.xpath('./competition')[0].text
	competitor1 = match.xpath('./competitors/competitor')[0].text
	competitor2 = match.xpath('./competitors/competitor')[1].text
	
	title =  competition + ": " + competitor1 + " vs. " + competitor2
	Log('-------------------------------')
	Log(title)
	Log('-------------------------------')
	
	url = match.xpath('./link')[0].text

	summary = match.xpath('./description')[0].text
	thumb_url = match.xpath('./thumb')[0].get('url')

	oc.add(CreateVideoClipObject(
	  title = title,
	  thumb = thumb_url,
	  url = url,
	  summary = summary
	))
		
  return oc

####################################################################################################
@route(PREFIX + '/createvideoclipobject', include_container=bool)
def CreateVideoClipObject(title, thumb, url, summary, include_container=False, *args, **kwargs):

  video_object = VideoClipObject(
    key=Callback(CreateVideoClipObject, title=title, thumb=thumb, url=url, summary=summary, include_container=True),
	rating_key=url,
	title=title,
	thumb=thumb,
	content_rating='NR',
	url=url,
	summary=summary,
	art=thumb,
	items=GetMediaObject(url)
	)
	
  if include_container:
	return ObjectContainer(objects=[video_object])
		
  else:
	return video_object

####################################################################################################
def GetMediaObject(url):

    mo = [
        MediaObject(
            parts=[PartObject(key=Callback(PlayVideo, url=url))],
			container = Container.MP4,
			video_codec = VideoCodec.H264,
			audio_codec = AudioCodec.AAC,
			audio_channels = 2,
			optimized_for_streaming = True
            )
        ]

    return mo

####################################################################################################
@indirect
# @route(PREFIX + '/playvideo.m3u8')
def PlayVideo(url, **kwargs):

    return IndirectResponse(VideoClipObject, key=url)

@Twoure
Probably better to have this discussion in a more related thread so following up on my last comment from http://forums.plex.tv/discussion/comment/1262254/#Comment_1262254 here…

I am able to play part vids as a single file (thanks to your code from here) but only if for the PartObject key I use the path to the actual file and not a link that satisfies the URLService for that host. I was hoping if there was a way that I could provide a link to the PartObject but then leave the parsing part to the URLService since otherwise I am repeating the parsing code and cannot use any URLService installed but limited to ones I customize. Any suggestion to circumvent this issue.

The other issue I have is there seems Plex has no way to jump to the next part unless I FF to the end of a part and then let it jump to next by itself. This seems also applicable to Movies in the Plex library which have multi-parts. The ‘Next’ button only works when it seems a PlayQue is running like when trailer or preroll is enabled for Movies in the library.

If you want to create a video with multiple parts that automatically play back to back as one long video, then you just need to define each part individually.

MediaObject(parts = [PartObject()], parts = [PartObject()], parts = [PartObject()])

Any URL service in the Services.bundle for a Viacom network website would show you good examples of building the parts for a media object. The Soutpark URL service is a good example for a single multi-resolution/bitrate HLS video in parts (github.com/plexinc-plugins/Services.bundle/blob/master/Contents/Service%20Sets/com.plexapp.plugins.southpark/URL/South%20Park/ServiceCode.pys) and Comedy Central shows you the method for an RTMP video in parts that offers different streams for each resolution option (github.com/plexinc-plugins/Services.bundle/blob/master/Contents/Service%20Sets/com.plexapp.plugins.comedycentral/URL/Comedy%20Central/ServiceCode.pys).

Be aware that all of the Viacom URL Services currently use @deffered in the MediObjectforURL function without a separate PlayVideo function to handle HTTP request. This allows the URL services to pull the data about the number of parts for each video in the MediObjectforURL function.This is done because the video for these Viacom network websites can have anywhere from 3 to 10 parts and requires an HTTP request to determine the number of parts.

But if you know the number of parts will not vary and can be hard coded, you can hard code the number of parts in the MediObjectforURL function without using the @deferred to facilitate an HTTP request in that function and use a separate PlayVideo function for those request. This can be seen in older versions of the URL services for Viacom network websites prior to April of 2014.

For example, here is an old SouthPark URL service that has the parts hard coded in the MediaObjecrforURL function. It defines two varying part lengths based on the format of the URL and loops through and builds each part - github.com/plexinc-plugins/Services.bundle/blob/e5e32792e6e800048268c60051b1daa248a8d113/Contents/Service%20Sets/com.plexapp.plugins.southpark/URL/South%20Park/ServiceCode.pys.

@coder-alpha said:
@Twoure
Probably better to have this discussion in a more related thread so following up on my last comment from http://forums.plex.tv/discussion/comment/1262254/#Comment_1262254 here…

I am able to play part vids as a single file (thanks to your code from here) but only if for the PartObject key I use the path to the actual file and not a link that satisfies the URLService for that host. I was hoping if there was a way that I could provide a link to the PartObject but then leave the parsing part to the URLService since otherwise I am repeating the parsing code and cannot use any URLService installed but limited to ones I customize. Any suggestion to circumvent this issue.
I am not sure what you are trying to do here. Are you trying to pull videos from various URL services to create your own multi-part media object? Though, I am not sure why anyone would want do to do that , you could use URLService.MediaObjectsForURL() to pull the media object data from other URL services into your own URL service.

Otherwise, you would need to be more specific about what you are trying to do. Maybe the examples I gave above of URL services with parts will provide what you are looking for.

The other issue I have is there seems Plex has no way to jump to the next part unless I FF to the end of a part and then let it jump to next by itself. This seems also applicable to Movies in the Plex library which have multi-parts. The ‘Next’ button only works when it seems a PlayQue is running like when trailer or preroll is enabled for Movies in the library.
The point of putting multiple parts in one media object is to play those parts as one complete video with each video part playing automatically when the previous part finishes. This is how all Plex player apps choose to play media objects with multiple parts.

The channel plugin code just creates the media objects and defines its parts and attributes. Each Plex player reads that media object data and chooses how it will play it. So, to choose a specific part of a multi-part media object to play or to skip past parts would have to be programmed as an option within the video player of each Plex player app individually by their developers.

If you wanted to play the parts independently from within the channel code, create a directory for each multi-part video and then create an individual media objects for each part within that directory, calling it Part one, Part two, etc.

Thanks @shopgirl284 for the examples…

So to give you an idea I’m trying to combine split-parts of a single episode as seen here. The issue I have is I need to have a generic solution which is compatible with different hosts eg. Dailymotion, Playwire, etc.

Based on the examples I have seen and tried, the PartObject needs the actual file url when constructing for multi-parts URLService.MediaObjectsForURL(). However, to make the solution independent I would like to use URLService of each host instead.

So the workflow would be

  1. Aggregating host urls from the page for each host, i.e. Dailymotion, Flash/Playwire, Letwatch, etc.
  2. Create VideoClipObject for each host and pass my N video links as a json string (which has a channel specific URLService to handle this)
  3. This channel specific URLService would then combine the part links

What I would prefer is if there was a way in step#3 above where I dont need to fetch the video file. I already have a URLService for Playwire. So I should be able to just pass the link. The same goes for Dailymotion which is provided natively by Plex and so on…

The end goal being - so instead if I can just pass the link to the PartObject where the URLService for each applicable host would kick in and do its job.

https://github.com/coder-alpha/DesiTelly.bundle is the work in progress but currently its only hardcoded for Playwire. I’m in the process of using @Twoure https://github.com/Twoure/UnSupportedServices.bundle/wiki/Host-list and so was keen to find a generic solution. As for now the general mode of playback does use individual media objects under a directory.

Thanks for the help.

PS: Thanks for clarifying about multi-part playback.

@tomasagray, I see from your example that you are choosing not to create a separate URL service. Here is a document that may be helpful explaining URL services - docs.google.com/document/d/1ePST3Dk0KywSmmDu3OPqpiNilpVgZDSzPgB0ns1yiFU. Below are some more specific examples of creating media objects within the channel init.py code and you would just replace the single part media objects with the multiple part media objects used in the URL service examples above.

HGTV is also a good example of channel without a separate URL service that uses a separate PlayVideo function to pull the media stream value - github.com/plexinc-plugins/HGTV.bundle/blob/master/Contents/Code/init.py#L176. The HGTV channel does not have a separate URL service because each video page has a playlist of multiple videos, not just one, so there is no way to define the URL pattern for each video page needed in a URL service. It includes a separate PlayVideo() function with @indirect because it must do an HTTP request to pull the value of the media stream URL.

But looking at your current code, it seems that the value of the URL sent to the CreateVideoClipObject() function from your main menu is the actual media stream, like a m3u8. If this is true, you do not need a separate PlayVideo() function with @indirect because you do not need to do an HTTP request to pull the media stream URL.

The RTUSA channel creates the media objects for its live feeds within the channel, because the actual media stream URL is pulled when we pull the metadata for those streams - github.com/plexinc-plugins/RTUSA.bundle/blob/master/Contents/Code/init.py#L115. As you see it does not have a separate PlayVideo() function because it already has the value of the media stream URL (an m3u8) before it is sent to the CreateVideoClipObject() function.

And though I am not aware of an example channel _init-.py without a separate URL service that uses @deferrred, you would only use @deferrred if you needed to do an HTTP request to define the values of your media objects (like the number of parts it has). You would just create a CreateVideoClipObject() function using @deferred and do all the HTTP request needed to define the media objects and to get the actual media stream URL in that one function.

Thanks for getting back to me @shopgirl284 . Sorry for the delay in my reply.

I see from your example that you are choosing not to create a separate URL service.

My channel is connected to a PHP script which outputs RSS with direct links to the media files. Is it necessary to create a URL service in this case? Also, my video files frequently contain multiple audio streams, each in a different language. Is it possible for my channel to tell Plex about these languages?

Thanks again!

@tomasagray said:
Thanks for getting back to me @shopgirl284 . Sorry for the delay in my reply.
My channel is connected to a PHP script which outputs RSS with direct links to the media files. Is it necessary to create a URL service in this case? Also, my video files frequently contain multiple audio streams, each in a different language. Is it possible for my channel to tell Plex about these languages?

As I said in that URL service document I linked above, Plex requires that you create a media object for each piece of media you want Plex to play.

You create a separate URL service when you want to define the url value/attribute in a metadata object, like VideoClipObject() or TrackObject(). When you define the url value/attribute in a metadata object, you are telling Plex you want to send that URL to a separate piece of code called a URL service. Then all the work to pull all the data and create media objects for any media from that website is done by the URL service whose ServiceInfo.plist URL pattern matches the URL you defined in the url value/attribute of that metadata object.

So all the media for each URL service must be from the same website and each piece of media from that website must have its own unique web page.

The biggest benefit of a separate URL service is that it allows you to separate the work of getting Plex to play the media from a specific website, which is always the hardest part of channel development and make sure Plex can play the media before you write the rest of your channel code.

Since you are using a RSS feed with multiple pieces of media that may be from different website and already lists the stream URLs and other attributes for each piece of media, it sounds like a separate URL service is not best and may not work for your project. But you will need to create a separate CreateVideoClipObject() function in your channel to create the the preplay screen (metadata) and media object for the various Plex player apps.

As to your question about language options, I am assuming that you are referring to each audio file having multiple language options.

There is a way to define language for a media object using the streams attribute/value of the PartObject(). This is included in the Missing From Documentation file - docs.google.com/document/d/1MyhhTsg5xdDD5LRbOZ5x5QxkmEyXv6rB-22Z7tdpy34/edit.

But the example given in that documentation is very simple, so it doesn’t do a good job of explaining how to list the key values for multiple language streams. I am looking into this so I understand it better and maybe we can make that example in the Missing Documentation more explanatory. I will let you know what I find.

So I checked on the language options, you should be able to create a media object for multiple language options with the following example code:

MediaObject( video_codec = VideoCodec.H264, audio_codec = AudioCodec.AAC, container = Container.MP4, audio_channels = 2, parts = [ PartObject( streams=[ AudioStreamObject( key=audiostream_url, language_code=Locale.Language.English ), AudioStreamObject( key=audiostream_url2, language_code=Locale.Language.French ) ] ) ] )

@shopgirl284 , thanks again for your helpful reply. I am making progress on this channel, thanks to your help.

The RSS feed contains links to the media files, all of which are in the same location (same server, same root directory), as well as metadata about the media files. There is minimal processing necessary to get the URL of the video file - it’s directly linked in the RSS, so I just pull the URL with xpath.

As it now stands, the preplay screens are fine, but I can only get the media to play if I’m playing it on a device with the same native resolution as the video. When it does play, it’s shaky - it buffers a lot, and I get errors saying that the server is not powerful enough to transcode smoothly. However, the server comfortably transcodes much beefier media without a hitch, so something else is going on.

If I specify the language_code attribute in AudioStreamObject (not sure where to get the audio stream URL attr), I get audio playback; if I omit it, silence. Audio is still listed as “Unknown” on the pre-play screen.

When the video does play, it only plays the first part. When I try to open the media via the Plex Windows Store app, I get an error:
Unexpected number of parts in indirect media.

The problem is, each video can have a variable number of parts - 1, 2, 3 or more - so I add them to the parts of the MediaObject with a loop. Perhaps this is what is tripping me up? Here is the complete revised init.py

#####################################################################
# Plex channel plugin for pre-recorded soccer matches. May some day
# be expanded to include other sports as well.

from datetime import datetime
import xml.etree.ElementTree as ET
import urllib

PREFIX = '/video/soccermatches'
TITLE = 'Soccer Matches'
ART = 'art-default.jpg'
ICON = 'icon-default.png'

MATCHES = 'matches.png'
COMPETITIONS = 'competitions.png'
TEAMS = 'teams.png'

SITE_ROOT = <someURL>

#####################################################################
# This (optional) function is initially called by the PMS framework to
# initialize the plug-in. This includes setting up the Plug-in static
# instance along with the displayed artwork.

def Start(): # Initialize the plug-in
  # Setup the default attributes for the ObjectContainer
  ObjectContainer.title1 = TITLE	
  ObjectContainer.art = R(ART)

  # Setup the default attributes for the other objects
  DirectoryObject.thumb = R(ICON)
  DirectoryObject.art = R(ART)
  VideoClipObject.thumb = R(ICON)
  VideoClipObject.art = R(ART)


@handler(PREFIX, TITLE)
def Main():
  oc = ObjectContainer(
	objects = [
		DirectoryObject(
			key = Callback(Matches),
			title = "All Matches",
			thumb = R(MATCHES)
		),
		
		DirectoryObject(
			key = Callback(Competitions),
			title = "Competitions"
		),
		
		DirectoryObject(
			key = Callback(Teams),
			title = "Teams"
		)
	]
  )
  
  return oc

#######################################################################################
# Add matches to the object container which is returned with metadata. 
# Limiting according to team, competition or both is accomplished in matches.rss.php
@route(PREFIX + '/matches')
def Matches(competition="ALL", team="ALL"):
  oc = ObjectContainer()
  oc.title1 = 'Matches'
  
  for match in XML.ElementFromURL(SITE_ROOT + 'matches.rss.php').xpath('//match'):
	competitionName = match.xpath('./competition')[0].get('name')
	competitor1 = match.xpath('./competition/competitor')[0].text
	competitor2 = match.xpath('./competition/competitor')[1].text
	
	# Setup metadata vars
	art_url = match.xpath('./competition')[0].get('background')
	match_duration = int(match.xpath('./media/metadata')[0].get('duration')) * 1000
	title = competitionName + ': ' + competitor1 + ' vs. ' + competitor2
	match_date = datetime.strptime(match.xpath('.')[0].get('date'), '%Y-%m-%d')
	url = match.xpath('./media/file')[0].get('url')
	Log('Match date: ' + str(match_date))

	oc.add (CreateVideoClipObject(
	  str_match = ET.tostring(match, encoding='utf8'),
	  title = title,
	  content_rating = 'NR',
	  duration = match_duration,
	  art = art_url,
	  thumb = art_url,
	  match_url = url + 'cccccccccc'
	))

  return oc

#####################################################################################################################
# Create the VideoClipObject with key and rating_key
@route(PREFIX + '/createvideoclipobject', duration=int)
def CreateVideoClipObject(str_match, title, content_rating, duration, art, thumb, match_url, include_container=False, *args, **kwargs):
  match = urllib.unquote_plus(str_match)
  
  video_object = VideoClipObject(
	key=Callback(CreateVideoClipObject, str_match=match, title=title, content_rating=content_rating, duration=duration, art=art, thumb=thumb, match_url=match_url, include_container=True),
	rating_key=match_url,
	title=title,
	content_rating=content_rating,
	items=GetMediaObject(match),
	duration=duration,
	art=Resource.ContentsOfURLWithFallback(url=art),
	thumb=Resource.ContentsOfURLWithFallback(url=thumb)
  )
	
  if include_container:
	return ObjectContainer(objects=[video_object])
		
  else:
	return video_object


########################################################################
@route(PREFIX + '/getmediaobject')
def GetMediaObject(str_match):
  match = XML.ElementFromString(str_match).xpath('//match')
  part_obj = []
  
  for file in match[0].xpath('./media/file'):
	url = file.xpath('.')[0].get('url')
	part_obj.append(
	  PartObject(
		key=Callback(
			PlayVideo, url=url
		),
		streams=[
		  AudioStreamObject(
			language_code=Locale.Language.Spanish
		  )
		]
	  )
	)
	
	Log('Soccer Matches: ' + url + ' added to MediaObject')

  mo = [
	MediaObject (
	  parts = part_obj,
	  container = GetMediaContainer(match[0].xpath('./media/metadata')[0].get('container')),
	  video_codec = GetVideoCodec(match[0].xpath('./media/metadata/video')[0].get('codec')),
	  audio_codec = GetAudioCodec(match[0].xpath('./media/metadata/audio')[0].get('codec')), 
	  audio_channels = int(match[0].xpath('./media/metadata/audio')[0].get('channels')),
	  optimized_for_streaming = False
	)
  ]
	
  return mo
	
##################################################################################
# Directory listing of competitions
@route(PREFIX + '/competitions')
def Competitions(name='ALL'):
  oc = ObjectContainer()
  oc.title1 = 'Competitions'
  
  # List competitions - DirectoryObject
  for competition in XML.ElementFromURL(SITE_ROOT + 'competitions.rss.php?name=' + name).xpath('//competition'):
	name = competition.xpath('.')[0].get('name')
	emblem = competition.xpath('./emblem')[0].get('url')
	art_url = competition.xpath('./background')[0].get('url')

	# Directory containing all matches in specified competition
	oc.add ( 
	  DirectoryObject (
		key = Callback(Matches, competition=name),
		title = name,
		thumb = emblem,
		art = art_url
	  )
	)
  
  return oc

@route(PREFIX + '/teams')
def Teams(team='ALL'):
  oc = ObjectContainer()
  oc.title1 = 'Teams'
  
  # List teams - DirectoryObject
  for team in XML.ElementFromURL(SITE_ROOT + 'teams.rss.php?name=' + team).xpath('//team'):
	name = team.xpath('.')[0].get('name')
	emblem = team.xpath('./emblem')[0].get('url')

	# Directory containing all matches in specified team
	oc.add ( 
	  DirectoryObject (
		key = Callback(Matches, team=name),
		title = name,
		thumb = emblem
	  )
	)
  
  return oc

  
  
########################################################################################
# These functions convert strings, gathered from XML (ultimately from FFMPEG JSON),
# into Plex's objects.
########################################################################################

########################################################################################
# Convert FFMPEG format_name to Container.* that Plex likes
@route(PREFIX + '/getmediacontainer')
def GetMediaContainer(container): 
  if container == 'mov,mp4,m4a,3gp,3g2,mj2' or container == 'mpegts':
   return Container.MP4
	
  elif container == 'matroska,webm':
   return Container.MKV

  elif container == 'avi':
   return Container.AVI
	

##########################################################################################
# Convert video codec_name to VideoCodec.*
@route(PREFIX + '/getvideocodec')
def GetVideoCodec(codec_name):
  # For now (10/2016), Plex appears to only support H264
  return VideoCodec.H264		# Default
  

##############################################################################################
# Convert audio codec_name to AudioCodec.*
@route(PREFIX + '/getaudiocodec')
def GetAudioCodec(codec_name):
  if codec_name == 'ac3' or codec_name == 'mp2'or codec_name == 'mp3':
	return AudioCodec.MP3

  else:
	return AudioCodec.AAC


####################################################################################################
# To actually play the video.
@route(PREFIX + '/playvideo.m3u8')
@indirect
def PlayVideo(url, **kwargs):
  Log('Playing: ' + url)
  return IndirectResponse(VideoClipObject, key=url)

UPDATE:
Changing
part_obj.append(
PartObject( key=Callback( PlayVideo, url=url)
)

to
part_obj.append(
PartObject( key=HTTPLiveStreamURL(url) )
)

improves playback for most files. It no longer buffers, nor displays the “server is too slow” message.

Also, I was wrong: Plex is serving all media files sequentially. I thought they would all be loaded together (i.e., 2 one-hour clips would be presented as a single 2hr clip with one track bar), but instead they play one after the other like a playlist (with a new trackbar each time).

However, it still only plays at native resolution (I can’t watch on a laptop or iPad, f’rex).

@tomasagray said:
UPDATE:
Changing
part_obj.append(
PartObject( key=Callback( PlayVideo, url=url)
)

to
part_obj.append(
PartObject( key=HTTPLiveStreamURL(url) )
)

improves playback for most files. It no longer buffers, nor displays the “server is too slow” message.

Also, I was wrong: Plex is serving all media files sequentially. I thought they would all be loaded together (i.e., 2 one-hour clips would be presented as a single 2hr clip with one track bar), but instead they play one after the other like a playlist (with a new trackbar each time).

However, it still only plays at native resolution (I can’t watch on a laptop or iPad, f’rex).

Hi, can you also please confirm that using PartObject with multiple files works on iPad ? I have a similar implementation in my plugin but using my iPhone I only get the last part to play. Thnx