Australian Parliament House Streams
In-Browser stream players are nice, but desktop players are better
✍️ Jacob Mulquin📅 30/11/2021
You can find the list of streams today (given it's a weekday) at APH Streams.
Every now and then I like to catch a bit of Question Time. A few days ago was one such occasion. It had been quite a while since I last visited this page and after pressing play on the House of Representatives link, it was my misfortune to discover that the player did not play nicely with my browser setup. My suspicion for the reason it does not work is that my browser triggers the stream to load in an entirely new tab and does not associate itself with the calling page.
Dissecting the web player
On the Watch, Read, Listen page, there is a table that contains all the streams for the day.
Each link contains an onclick
handler that opens /News_and_Events/LiveMediaPlayer?vID={vID}
. This vID
appears to be generated by the APH content management system and is unique for every single stream that occurs.
On each LiveMediaPlayer
page, an iframe is included from the company Switch Media that broadcasts the actual stream. There is also a simple jQuery handler to grab a "currently playing" text from an APH server. A sample URL: https://api-v3.switchmedia.asia/switch.tv/vcms/wrapper.html.php?videoID={videoId}&siteID={siteID}
. The APH siteID
appears to be 277.
The Switch Media page includes some javascript at https://api-v3.switchmedia.asia/switch.tv/vcms/wrapper.js.php?siteID={siteID}
.
And we've hit the meat and potatos of this live stream player script. This wrapper.js.php
file contains references to the following API URLs:
window.playlistUrl = "/{SITEID}/playlists/get-data?playlist={PLAYLISTID}&detail=verbose&format=json";
window.assetUrl = "/{SITEID}/assets/get-data?asset={VIDEOID}&detail=verbose&format=json";
window.channelUrl = "/{SITEID}/channels/get-data?channel={CHANNELID}&detail=verbose&format=json";
The assetUrl
looks promising, let's build a link: https://api-v3.switchmedia.asia/277/assets/get-data?asset=1172568&detai0=verbosel&format=json
Unfortunately this is a dead end:
[
{
"_type": "assetinfo",
"asset": "1172568",
"title": "House of Representatives 101",
"synopsis": "Live Stream: HOR 101ext - Channel 1 for event, HOR 101ext - Channel 1. HOR 101ext - Channel 1",
"description": "House of Representatives Chamber",
"shortdescription": "",
"duration": "0",
"uploaded": "2016-02-11 11:05:36",
"created": "2016-02-11 11:05:36",
"published": "2016-03-23 18:01:12",
"unpublished": null,
"expired": "0000-00-00 00:00:00",
"reference": "",
"state": "APPROVED",
"image": "https://downloads.switchmedia.asia/filestore/getimage.php?siteID=277&videoID=1172568&group=STILL&tag=IMAGE&f=jpg&w=200&q=100&returnDefault=true&fileID=",
"categorylist": {
"_type": "categories",
"categories": []
},
"bookmarklist": {
"_type": "bookmarks",
"bookmarks": []
},
"playlistlist": {
"_type": "playlists",
"playlists": []
}
}
]
Just under the API calls is a link described as "Universal player configuration url", this also looks promising.
window.playerConfigUrl = "/{SITEID}/playback/getUniversalPlayerConfig?videoID={VIDEOID}&playlistID={PLAYLISTID}" +
"&skinType=vcms&profile=" + window.profile + "&playerID=" + window.playerId +
"&format=json&bookmarkID={BOOKMARKID}&autoplay={AUTOPLAY}";
And we build as so: https://api-v3.switchmedia.asia/277/playback/getUniversalPlayerConfig?videoID=1172568&playlistID=0&skinType=vcms&profile=regular&playerID=playerregular&format=json&bookmarkID=0&autoplay=true
Ding! Ding! Ding! We have a winner!
{
"_type": "playback",
"autoPlayOnSeek": "false",
"skin": {
"_type": "skin",
"osd": {
"_type": "osd",
"infoContainer": "true"
},
"startFullscreen": "false",
"hideOsdTimeout": "3000",
"hideOsdInstantly": "false",
"showAdRemaining": "true",
"showOsd": "true",
"rampedSeek": "true",
"noUserInput": "true",
"closeOnLastPlayed": "false",
"HTML": "https://api-v3.switchmedia.asia/277/skin/getSkin?file=vcms"
},
"components": {
"_type": "components",
"player": "urn:tv.switch.player",
"skin": "urn:tv.switch.skin",
"playlist": "urn:tv.switch.playlist",
"bookmarks": "urn:tv.switch.bookmarks.json",
"captions": "urn:tv.switch.captions.webvtt"
},
"media": {
"_type": "media",
"aspect": "16:9",
"duration": "0",
"autoplay": "false",
"staticImage": "https://downloads.switchmedia.asia/filestore/getimage.php?siteID=277&videoID=1172568&group=STILL&tag=IMAGE&f=jpg&w=1366&returnDefault=true&fileID=",
"downloadButton": "true",
"shareButton": "false",
"renditions": [
{
"_type": "array",
"audioOnly": "false",
"url": "https://dps-live-hls.global.ssl.fastly.net/hls/277_HOR101_18000.m3u8",
"videoContainer": "HLSURL",
"streamType": "LIVE",
"quality": "Auto"
},
{
"_type": "array",
"audioOnly": "false",
"url": "https://dps-live-hls.global.ssl.fastly.net/hls/277_HOR101_18000_03.m3u8",
"videoContainer": "HLSURL",
"streamType": "LIVE",
"quality": "467 Kbps"
},
{
"_type": "array",
"audioOnly": "false",
"url": "https://dps-live-hls.global.ssl.fastly.net/hls/277_HOR101_18000_02.m3u8",
"videoContainer": "HLSURL",
"streamType": "LIVE",
"quality": "722 Kbps"
},
{
"_type": "array",
"audioOnly": "false",
"url": "https://dps-live-hls.global.ssl.fastly.net/hls/277_HOR101_18000_01.m3u8",
"videoContainer": "HLSURL",
"streamType": "LIVE",
"quality": "1029 Kbps"
}
],
"state": "APPROVED",
"expired": "0000-00-00 00:00:00",
"type": "LIVE_VIDEO",
"assetID": "1172568",
"sessionID": "0",
"timeMap": {
"_type": "timeMap",
"source": {
"_type": "source",
"timecode_timezone": "39600",
"timecode_delay": "45"
}
},
"captions": [],
"synopsis": "Live Stream: HOR 101ext - Channel 1 for event, HOR 101ext - Channel 1. HOR 101ext - Channel 1",
"title": "",
"cuePointMetaData": [],
"liveStreamSlates": {
"_type": "liveStreamSlates",
"MEDIA_ERROR": "https://downloads.switchmedia.asia/filestore/getimage.php?siteID=277&videoID=1172568&group=STILL=SLATE_MEDIA_ERROR&f=jpg&w=1366&returnDefault=true&fileID=",
"NO_STREAM": "https://downloads.switchmedia.asia/filestore/getimage.php?siteID=277&videoID=1172568&group=STILL=SLATE_NO_STREAM&f=jpg&w=1366&returnDefault=true&fileID="
},
"playbackMode": "live"
},
"ads": {
"_type": "ads",
"skipAdsDuringSeek": "true",
"controls": {
"_type": "controls",
"pause": "true",
"seek": "false",
"stop": "false"
},
"playLastSkippedAd": "true"
},
"analytics": {
"_type": "analytics",
"userID": 0,
"userRole": null,
"siteID": "277",
"assetID": "1172568",
"externalID": null,
"referrer": "",
"playbackSessionId": "0000:61a4a6827332d",
"url": "https://api-v3.switchmedia.asia/277/analytics/",
"saUrl": "",
"filters": {
"_type": "filters",
"ad_firstQuartile": 1,
"ad_midpoint": 1,
"ad_thirdQuartile": 1,
"ad_complete": 1,
"ad_clickTracking": 1,
"ad_clicktracking": 1,
"ad_closeLinear": 1,
"ad_fullscreen": 1,
"ad_exitFullscreen": 1,
"ad_pause": 1,
"ad_resume": 1,
"ad_mute": 1,
"ad_unmute": 1,
"ad_rewind": 1
},
"custom": []
},
"sessionDataUrl": "https://api-v3.switchmedia.asia/277/playback/getSessionData?sessionID=0&videoID=1172568&subs=&cl=&streamFormat=hls&format=json"
}
Hooray, we have m3u8 files, let's try using mpv https://dps-live-hls.global.ssl.fastly.net/hls/277_HOR101_18000.m3u8
Soooo much better than a silly javascript player.
Doing this daily
It appears that the m3u8 files do not change day to day, which is fine if I only wanted to capture the House of Representatives and Senate streams. However, I wanted to make sure that I could find the streams of each thing going on each day. This means that web scraping is required, which means good old python.
I won't bore you with the full details of the implementation, but basically it scrapes the table of daily events, then extracts the vIDs
, loads the APH LiveMediaPlayer page with each vID
and from those pages and extracts the videoID
from the iframe
element. Then it constructs the Switch Media page getUniversalPlayerConfig
using that videoID
. From there it builds a simple HTML table.
Since this host does not come with Python, I run this script at home and automated the upload via cron
. You can see the page at APH Streams.
Future Improvements
One other thing about the Watch, Read, Listen page that is slightly annoying, is that each livestream has a little "live" badge next to it. This badge is slightly disingenous, as one would expect that this only appears if the video was live at the moment, not that it is a live video in general.
To remedy this, I would like to add some javascript to the generated HTML that would detect whether the current time is before, within or after the time range of each stream.
It's very much a nice-to-have, so most likely will happen in... a year or two?