@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)