Jacob Mulquin

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.

Firefox console showing errors

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 +

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

Livestream of APH running in mpv

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?