<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sebastian Ljungman</title>
    <description>The latest articles on DEV Community by Sebastian Ljungman (@sebastianljungman).</description>
    <link>https://dev.to/sebastianljungman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F821065%2F5fc82086-9ffe-4fdb-99d5-f0e67fc0a312.jpeg</url>
      <title>DEV Community: Sebastian Ljungman</title>
      <link>https://dev.to/sebastianljungman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sebastianljungman"/>
    <language>en</language>
    <item>
      <title>End-to-end testing with our Chaos Stream Proxy</title>
      <dc:creator>Sebastian Ljungman</dc:creator>
      <pubDate>Wed, 22 Jun 2022 15:55:51 +0000</pubDate>
      <link>https://dev.to/video/end-to-end-testing-with-our-chaos-stream-proxy-45cc</link>
      <guid>https://dev.to/video/end-to-end-testing-with-our-chaos-stream-proxy-45cc</guid>
      <description>&lt;p&gt;Testing the behavior of a video player when there is &lt;strong&gt;something wrong with the video source&lt;/strong&gt; used to be a tedious task: You had to either manually create or hunt down different broken streams to test different errors, and even then you couldn't always know for sure that the same error would always occur with the same timing.&lt;/p&gt;

&lt;p&gt;To address this problem, Eyevinn has released its &lt;strong&gt;Chaos Stream Proxy&lt;/strong&gt; as &lt;a href="https://github.com/Eyevinn/chaos-stream-proxy" rel="noopener noreferrer"&gt;open-source&lt;/a&gt;! As the name suggests, this is a &lt;strong&gt;very handy tool&lt;/strong&gt; for proxying an adaptive bitrate stream and &lt;strong&gt;deterministically&lt;/strong&gt; introducing corruptions to it. &lt;/p&gt;

&lt;p&gt;This tutorial will demonstrate how to use the Proxy in the end-to-end testing of the open-source &lt;a href="https://github.com/Eyevinn/web-player" rel="noopener noreferrer"&gt;Eyevinn WebPlayer&lt;/a&gt;, using &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Chaos Stream Proxy
&lt;/h2&gt;

&lt;p&gt;A demo of the Chaos Stream Proxy, which we'll use for the purpose of this tutorial, is currently running &lt;a href="https://chaos-proxy.prod.eyevinn.technology" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Proxy supports both HLS &lt;strong&gt;and&lt;/strong&gt; MPEG-DASH streaming formats, and easily lets us add different corruptions by adding a stringified JSON object as a &lt;strong&gt;query parameter&lt;/strong&gt; to the proxied URL. If we take this HLS URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it can be played with no problems, for example in the Eyevinn WebPlayer, &lt;a href="https://web.player.eyevinn.technology/" rel="noopener noreferrer"&gt;hosted here&lt;/a&gt;. But, if we proxy it through the Chaos Stream Proxy, and set &lt;code&gt;&amp;amp;statusCode=[{i:*,code:404}]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8&amp;amp;statusCode=[{i:*,code:404}]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we'll instead get &lt;code&gt;404&lt;/code&gt; errors for all segments (because we set &lt;code&gt;i:*&lt;/code&gt;; it's also possible to specify individual segments).&lt;/p&gt;

&lt;p&gt;For specifying other types of corruptions, check out &lt;a href="https://github.com/Eyevinn/chaos-stream-proxy" rel="noopener noreferrer"&gt;the project's README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  EPAS and the Eyevinn WebPlayer
&lt;/h2&gt;

&lt;p&gt;The Eyevinn WebPlayer implements &lt;a href="https://github.com/Eyevinn/player-analytics-specification" rel="noopener noreferrer"&gt;Eyevinn Player Analytics Specification (EPAS)&lt;/a&gt;, which is an open specification that defines a standard for implementing analytics in any video/audio player. This means that we have a format for event reporting that we expect the WebPlayer to follow. &lt;/p&gt;

&lt;p&gt;User interactions are easy enough to create for the sake of validating the EPAS implementation, and thanks to the Chaos Stream Proxy, manifest related errors are now also a piece of cake!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Playwright
&lt;/h2&gt;

&lt;p&gt;Playwright is a powerful end-to-end test runner, which we're using to automatically test our code in a real browser environment. At the time of writing, we're adding a dependency for &lt;code&gt;"@playwright/test": "^1.19.2"&lt;/code&gt; in our &lt;code&gt;package.json&lt;/code&gt;(this is a Node.js project). We also add the script &lt;code&gt;"test:e2e": "playwright test"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;playwright.config.ts&lt;/code&gt; is in the project root, and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PlaywrightTestConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlaywrightTestConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;forbidOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tests/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on-first-retry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Necessary to get the media codecs to play video (default 'chromium' doesn't have them) &lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;webServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm run examples&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reuseExistingServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;testDir&lt;/code&gt; points to the folder where we put our test &lt;code&gt;.spec.ts&lt;/code&gt; files; &lt;code&gt;examples&lt;/code&gt; in the &lt;code&gt;npm run examples&lt;/code&gt; command is a folder in our root directory, in which we put the HTML files that Playwright will use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; that &lt;code&gt;channel: 'chrome'&lt;/code&gt; is necessary for our tests to run; the default setting is &lt;code&gt;chromium&lt;/code&gt;, &lt;a href="https://playwright.dev/docs/browsers#when-to-use-google-chrome--microsoft-edge-and-when-not-to" rel="noopener noreferrer"&gt;which doesn't include the necessary media codecs for video playback &lt;/a&gt;. This may cause issues with automated workflows and running tests on Firefox and Webkit, but works locally in Chrome.&lt;/p&gt;

&lt;p&gt;In our &lt;code&gt;examples&lt;/code&gt; folder, we add an &lt;code&gt;index.html&lt;/code&gt; HTML file that includes a link to our Chaos Stream Proxy HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"X-UA-Compatible"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"IE=edge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Examples&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"chaos-proxy/index.html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Chaos Stream Proxy Example&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As well as the Chaos Stream Proxy HTML &lt;code&gt;index.html&lt;/code&gt; file in the sub-folder &lt;code&gt;chaos-proxy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"X-UA-Compatible"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"IE=edge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Chaos Stream Proxy Example&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"../../packages/web-player-component/dist/web-player.component.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- VOD: With segment delay of 1500ms and response code 400 on sixth (response of 400 will be sent after 1500ms): --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;eyevinn-video&lt;/span&gt;
    &lt;span class="na"&gt;source=&lt;/span&gt;&lt;span class="s"&gt;"https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8&amp;amp;delay=[{i:5,ms:1500}]&amp;amp;statusCode=[{i:5,code:400}]"&lt;/span&gt;
    &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/eyevinn-video&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- VOD: With response of status code 404 on all segments: --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;eyevinn-video&lt;/span&gt;
  &lt;span class="na"&gt;source=&lt;/span&gt;&lt;span class="s"&gt;"https://chaos-proxy.prod.eyevinn.technology/api/v2/manifests/hls/proxy-master.m3u8?url=https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8&amp;amp;statusCode=[{i:*,code:404}]"&lt;/span&gt;
  &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/eyevinn-video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file imports the compiled version of &lt;a href="https://dev.to/video/creating-a-video-player-web-component-3lgg"&gt;Eyevinn WebPlayer Component&lt;/a&gt; from the repository, which let's us embed the WebPlayer as web components with the &lt;code&gt;&amp;lt;eyevinn-video&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;In our &lt;code&gt;tests&lt;/code&gt; folder, we add &lt;code&gt;chaos-proxy.specs.ts&lt;/code&gt;, which will be run automatically when we run the &lt;code&gt;npm run test:e2e&lt;/code&gt; script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;player sends error events when loading corrupt streams&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/chaos-proxy/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://sink.epas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postDataJSON&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postDataJSON&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;400&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://sink.epas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postDataJSON&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postDataJSON&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;404&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We make asynchronous calls to the &lt;a href="https://playwright.dev/docs/api/class-page#page-goto" rel="noopener noreferrer"&gt;goto&lt;/a&gt; and &lt;a href="https://playwright.dev/docs/api/class-page#page-wait-for-request" rel="noopener noreferrer"&gt;waitForRequest&lt;/a&gt; methods, available on the &lt;code&gt;page&lt;/code&gt;object. We check that the POST &lt;a href="https://playwright.dev/docs/api/class-request" rel="noopener noreferrer"&gt;requests&lt;/a&gt; conform to the expected format: If both matching requests are made within 30 seconds, the test passes!&lt;/p&gt;

&lt;p&gt;We run the test with &lt;code&gt;npm run test:e2e&lt;/code&gt;, and this should be the result: &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfj73q04apkgx2k7x78w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqfj73q04apkgx2k7x78w.png" alt="Test ran successfully"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Success!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Playwright Test for VSCode and debug mode
&lt;/h2&gt;

&lt;p&gt;Playwright is developed my Microsoft, so perhaps it shouldn't come as a surprise that there is &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright" rel="noopener noreferrer"&gt;an excellent VS Code extension available&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this extension, it's a breeze to run individual tests in isolation, add breakpoints and more! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5457mfqi6e39kcs1joex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5457mfqi6e39kcs1joex.png" alt="Playwright Test for VS Code screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also gives easy access to debug mode, which lets us view and interact with the test in the test runner browser: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh5oj6n04wknqjzbfh79.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh5oj6n04wknqjzbfh79.png" alt="Debug mode screenshot"&gt;&lt;/a&gt; &lt;/p&gt;

</description>
      <category>testing</category>
      <category>tutorial</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Creating an Apple TV app with SwiftUI</title>
      <dc:creator>Sebastian Ljungman</dc:creator>
      <pubDate>Wed, 25 May 2022 08:57:14 +0000</pubDate>
      <link>https://dev.to/video/creating-an-apple-tv-app-with-swiftui-51fa</link>
      <guid>https://dev.to/video/creating-an-apple-tv-app-with-swiftui-51fa</guid>
      <description>&lt;p&gt;In 2019, Apple launched its new UI paradigm &lt;a href="https://developer.apple.com/xcode/swiftui/" rel="noopener noreferrer"&gt;SwiftUI&lt;/a&gt;, which is meant to replace the existing Interface Builder. It lets us create apps for all Apple platforms, including Apple TV's &lt;strong&gt;tvOS&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;In this tutorial, we'll see how we can build a tvOS app that loads HLS streams from XML/MRSS files, displays them to the user for selection and plays back the selected stream. The streams will be specified in an &lt;code&gt;.xcconfig&lt;/code&gt; configuration file, and either bundled at buildtime or downloaded at runtime depending on if local files or &lt;code&gt;http(s)://&lt;/code&gt; URL:s are specified.&lt;/p&gt;

&lt;p&gt;Tools we'll be using include &lt;a href="https://developer.apple.com/xcode/" rel="noopener noreferrer"&gt;Xcode&lt;/a&gt; and the CocoaPods package &lt;a href="https://cocoapods.org/pods/SWXMLHash" rel="noopener noreferrer"&gt;SWXMLHash&lt;/a&gt;, for parsing our XML input.    &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi3828vnao460u58k33t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdi3828vnao460u58k33t.png" alt="Video selection menu"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Being a relatively new framework, the number of learning resources for SwiftUI are still smaller than for Interface Builder, and the ones that &lt;strong&gt;are&lt;/strong&gt; available tend to focus on the iOS platform. Fortunately though, there are very few things which differ when using SwiftUI: UI elements and user interactions are automatically suited to the platform we're building for.&lt;/p&gt;

&lt;p&gt;If you're new to Swift, I recommend following along with the playground in &lt;a href="https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html" rel="noopener noreferrer"&gt;Apple's tutorial&lt;/a&gt;. To learn SwiftUI, Apple has an excellent &lt;a href="https://developer.apple.com/tutorials/swiftui" rel="noopener noreferrer"&gt;tutorial project&lt;/a&gt;, which goes through many important features. For understanding this tutorial though, the first chapter, &lt;strong&gt;SwiftUI Essentials&lt;/strong&gt;, should suffice. The guide is for iOS, but I was able to follow the steps for tvOS and get a working app, with only minor changes. &lt;/p&gt;

&lt;p&gt;Although I will include some important snippets of code in this blog post, I recommend that you &lt;a href="https://github.com/Eyevinn/tvos-swift-example-app" rel="noopener noreferrer"&gt;clone the project&lt;/a&gt; to get the full picture. As with many of our cool projects here at Eyevinn, the code is &lt;strong&gt;open source&lt;/strong&gt;, so feel free to fork and play around with it! &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up
&lt;/h2&gt;

&lt;p&gt;We start by creating an Xcode project for the app, making sure we select &lt;code&gt;tvOS&lt;/code&gt; as our platform. I've used the project name &lt;code&gt;EyevinnTV&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;We also set up SWXMLHash in the same project folder by following &lt;a href="https://cocoapods.org/pods/SWXMLHash" rel="noopener noreferrer"&gt;these instructions&lt;/a&gt;. If you're using an M1 Mac, it &lt;strong&gt;may&lt;/strong&gt; be easier installing CocoaPods through Brew than RubyGems (at least it was for me).&lt;/p&gt;

&lt;p&gt;To open the project together with the SWXMLHash dependency, we use the &lt;code&gt;.xcworkspace&lt;/code&gt; file created after running &lt;code&gt;pod install&lt;/code&gt; rather than the &lt;code&gt;.xcodeproj&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the ContentView
&lt;/h2&gt;

&lt;p&gt;SwiftUI is all about different nested &lt;code&gt;Views&lt;/code&gt;, and the main &lt;code&gt;View&lt;/code&gt; in our app is the &lt;code&gt;ContentView&lt;/code&gt;, which is called in the &lt;code&gt;EyevinnTVApp.swift&lt;/code&gt; file. The &lt;code&gt;EyevinnTVApp&lt;/code&gt; struct in this file conforms to the &lt;code&gt;App&lt;/code&gt; protocol, and is preceded by the &lt;code&gt;@main&lt;/code&gt; attribute, indicating that this is the entry point into our app. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qtmoipgqf3pbd3jp2e9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qtmoipgqf3pbd3jp2e9.png" alt="EyevinnTVApp"&gt;&lt;/a&gt;   &lt;/p&gt;

&lt;p&gt;Besides the automatically generated &lt;code&gt;ContentView&lt;/code&gt;, we also create a &lt;code&gt;VideoItemObject.swift&lt;/code&gt; file, and &lt;code&gt;VideoView.swift&lt;/code&gt; file. The former will contain a simple Class that represents our video objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;VideoItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Identifiable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;videoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;thumbnailUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that it should conform to the &lt;code&gt;Identifiable&lt;/code&gt; protocol, as this will let us iterate over an array of &lt;code&gt;VideoItem&lt;/code&gt; with &lt;code&gt;ForEach&lt;/code&gt; loops.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;ContentView&lt;/code&gt;, we import SWXMLHash, and define a class &lt;code&gt;videoUrlObject&lt;/code&gt; that conforms to &lt;code&gt;ObservableObject&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nv"&gt;videoUrlObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ObservableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@Published&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentVideoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="kd"&gt;@Published&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentVideoTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conforming to &lt;code&gt;ObservableObject&lt;/code&gt; in the parent &lt;code&gt;View&lt;/code&gt; is important, as this will let us pass in the &lt;code&gt;currentVideoUrl&lt;/code&gt; with the &lt;code&gt;@Published&lt;/code&gt; attribute to its child &lt;code&gt;View&lt;/code&gt; &lt;code&gt;VideoView&lt;/code&gt;, and synchronize changes to it by making it an &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects" rel="noopener noreferrer"&gt;ObservedObject&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@ObservedObject&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentVideoUrlObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;videoUrlObject&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;ContentView&lt;/code&gt; we create two state arrays of &lt;code&gt;VideoItem&lt;/code&gt; objects, to which we will push the video items after parsing them. We also initialize our &lt;code&gt;videoUrlObject&lt;/code&gt; with the &lt;code&gt;@StateObject&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;videoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;VideoItem&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;liveVideoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;VideoItem&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
    &lt;span class="kd"&gt;@StateObject&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentVideoUrlObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;videoUrlObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;body&lt;/code&gt; of &lt;code&gt;ContentView&lt;/code&gt;, we nest the different child &lt;code&gt;Views&lt;/code&gt; that make up our UI; a main &lt;code&gt;NavigationView&lt;/code&gt;, with either the video selection menu &lt;strong&gt;or&lt;/strong&gt; &lt;code&gt;VideoView&lt;/code&gt; being rendered, depending on whether the &lt;code&gt;currentVideoUrl&lt;/code&gt; variable is set to en empty string (default) or the URL of one of the video items. &lt;/p&gt;

&lt;p&gt;In the video selection menu, we display two carousels of videos, one intended for &lt;strong&gt;VOD&lt;/strong&gt; content, and the other for &lt;strong&gt;live&lt;/strong&gt; content. Each video object is rendered as a &lt;code&gt;Button View&lt;/code&gt;, displaying its thumbnail (as an &lt;a href="https://wwdcbysundell.com/2021/using-swiftui-async-image/" rel="noopener noreferrer"&gt;AsyncImage&lt;/a&gt;) and title. When clicking the Button, the &lt;code&gt;currentVideoUrl&lt;/code&gt; property of the &lt;code&gt;currentVideoUrlObject&lt;/code&gt; is set to the &lt;code&gt;videoUrl&lt;/code&gt; of the corresponding &lt;code&gt;VideoItem&lt;/code&gt;; this triggers a reload of the content inside our &lt;code&gt;Navigation View&lt;/code&gt;, and the &lt;code&gt;VideoView&lt;/code&gt; is rendered in place of the video selection menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshbrsitghdeo5q43kc36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshbrsitghdeo5q43kc36.png" alt="VideoView"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the VideoView
&lt;/h2&gt;

&lt;p&gt;Besides creating the &lt;code&gt;ObservedObject&lt;/code&gt;, we also &lt;code&gt;import AVKit&lt;/code&gt; to create an instance of the &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-play-movies-with-videoplayer" rel="noopener noreferrer"&gt;VideoPlayer View&lt;/a&gt;. On creation, we use the &lt;code&gt;onAppear()&lt;/code&gt; hook to load the &lt;code&gt;currentVideoUrl&lt;/code&gt; of the &lt;code&gt;ObservedObject&lt;/code&gt;. It's also &lt;strong&gt;essential&lt;/strong&gt; that we set a frame size of 1920X1080 points; this lets the player know that we're displaying the video in &lt;strong&gt;fullscreen&lt;/strong&gt;. If we don't, &lt;strong&gt;no playback controls will be displayed&lt;/strong&gt;!   &lt;/p&gt;

&lt;p&gt;We also override the default behavior of pressing the Back/ Menu button on the remote, to set the value of the &lt;code&gt;currentVideoUrl&lt;/code&gt; property to an empty string. This will trigger a reload, replacing the &lt;code&gt;VideoView&lt;/code&gt; with the video selection menu in our &lt;code&gt;ContentView&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the build configuration
&lt;/h2&gt;

&lt;p&gt;In a SwiftUI app, necessary project files are bundled at buildtime. This means that we can include our lists of videos  as XML files, which will be available at runtime through &lt;code&gt;Bundle.main.path&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myVideos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ofType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this project, we also want to support input in the form of an &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt; URL, or a &lt;code&gt;file://&lt;/code&gt; URL that points to an XML file somewhere on the compiling computer, which we make available at runtime. &lt;/p&gt;

&lt;p&gt;To enable this, we'll create an &lt;code&gt;Config.xcconfig&lt;/code&gt; build configuration file that sets the values of keys in a dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VOD_XML = https:\/\/testcontent.mrss.eyevinn.technology/
LIVE_XML = liveTestContentMrss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we're &lt;strong&gt;escaping the double backslashes&lt;/strong&gt; in URL:s. This is because &lt;strong&gt;"//"&lt;/strong&gt; signifies the start of a comment in &lt;code&gt;.xcconfig&lt;/code&gt; files, which is not what we want! The backslashes still become part of the value though, so we'll need to remove them later to get the correct URL. If our URL:s contain &lt;strong&gt;spaces&lt;/strong&gt;, we may also have to replace these with &lt;code&gt;%20&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll also need to add these entries to the project's &lt;code&gt;Info.plist&lt;/code&gt;, which is used to store build settings for the target: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmmntaqwi0jrr70bdjhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmmntaqwi0jrr70bdjhd.png" alt="Info.plist"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After building the project, we can access the set values with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infoDictionary&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"VOD_XML"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as!&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replacingOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are removing the backslashes with &lt;code&gt;replacingOccurences&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy files from file:// URL:s
&lt;/h2&gt;

&lt;p&gt;Now we can access both bundled files and remote URL:s, but what about local files outside the project? For this, we're going to write a &lt;a href="https://www.raywenderlich.com/25816315-using-swift-scripts-with-xcode" rel="noopener noreferrer"&gt;Swift script&lt;/a&gt; that detects &lt;code&gt;file://&lt;/code&gt; URL:s, reads and copies their content to bundled XML files at buildtime.&lt;/p&gt;

&lt;p&gt;We add a new Swift file, in my case &lt;code&gt;Scripts/CopyXml.swift&lt;/code&gt;, and create a new &lt;strong&gt;build phase&lt;/strong&gt; in which we compile and run our script:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuew5oskvhbj956cwqtsa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuew5oskvhbj956cwqtsa.png" alt="Build phases"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that the &lt;strong&gt;order&lt;/strong&gt; of the build phases matters, as the script needs to be run &lt;strong&gt;before&lt;/strong&gt; the &lt;code&gt;Copy Bundle Resources&lt;/code&gt; phase.&lt;/p&gt;

&lt;p&gt;In case the URL entered by the user is invalid, we define an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nv"&gt;fileReadError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;invalidFileUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add the &lt;code&gt;throws&lt;/code&gt; keyword to the &lt;code&gt;main&lt;/code&gt; function of the file.&lt;/p&gt;

&lt;p&gt;We still want to read the values of &lt;code&gt;VOD_XML&lt;/code&gt; and &lt;code&gt;LIVE_XML&lt;/code&gt; in our &lt;code&gt;Info.plist&lt;/code&gt;, but as we've yet to create our bundle, &lt;code&gt;Bundle.main.infoDictionary&lt;/code&gt; is not available to us. Instead, we'll use &lt;code&gt;ProcessInfo.processInfo.environment["VOD_XML"]&lt;/code&gt; to access it as an environmental variable. We'll also access the  location of our project's root directory with the key &lt;code&gt;SRCROOT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We then check if the values start with the &lt;code&gt;file://&lt;/code&gt; protocol, in which case we attempt to load the contents of the file. If the loading fails, we throw the &lt;code&gt;fileReadError&lt;/code&gt; that we defined. The error message will be printed in the &lt;strong&gt;build log&lt;/strong&gt; rather than the console, as will any other error messages/prints that may occur when running buildtime scripts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jnj07f7k3xeqqupsap6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jnj07f7k3xeqqupsap6.png" alt="Build log with error"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the loading is successful, we construct a &lt;code&gt;file://&lt;/code&gt; URL pointing to either &lt;code&gt;liveContentCopy.xml&lt;/code&gt; or &lt;code&gt;vodContentCopy.xml&lt;/code&gt;, depending on if the input &lt;code&gt;file://&lt;/code&gt; URL is in &lt;code&gt;LIVE_XML&lt;/code&gt; or &lt;code&gt;VOD_XML&lt;/code&gt;. These files should be in the &lt;strong&gt;project root&lt;/strong&gt; for the project to work, and copied to the app bundle in the &lt;code&gt;Copy Bundle Resources&lt;/code&gt; phase. We write to these &lt;code&gt;file://&lt;/code&gt; URL with &lt;code&gt;data.write(to: bundleFileUrl!)&lt;/code&gt;, which either overwrites the existing file's data, or creates a new one if the file is missing. &lt;/p&gt;

&lt;p&gt;It's worth noting that even if the files exist in the &lt;strong&gt;file system&lt;/strong&gt; (i.e. were added through Finder, the terminal or in our code), they are not automatically &lt;strong&gt;referenced&lt;/strong&gt; in the Xcode project. If we were to delete these files through the Xcode navigator and pass file URL:s in our &lt;code&gt;Config.xcconfig&lt;/code&gt;, the &lt;code&gt;data.write&lt;/code&gt; call in our code would create the new files in the file system, &lt;strong&gt;but&lt;/strong&gt; Xcode would not see the files.&lt;/p&gt;

&lt;p&gt;The solution is to add the files in the Project Navigator &lt;strong&gt;inside Xcode&lt;/strong&gt;, as this will create a reference to the files, so that they can be copied to the app bundle in the &lt;code&gt;Copy Bundle Resources&lt;/code&gt; phase. &lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing XML/MRSS input
&lt;/h2&gt;

&lt;p&gt;We should now have our input, either as an &lt;code&gt;http(s)://&lt;/code&gt; URL, or the name of a bundled file (either added directly and referred to by name, or copied from en external &lt;code&gt;file://&lt;/code&gt; URL to either &lt;code&gt;liveContentCopy&lt;/code&gt; or &lt;code&gt;vodContentCopy&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We want our app to support &lt;a href="https://www.rssboard.org/media-rss#media-content" rel="noopener noreferrer"&gt;MRSS feeds&lt;/a&gt;, which is a standard for distributing media over RSS. Here's an example of what &lt;code&gt;liveContentCopy&lt;/code&gt; might look like, after reading from a &lt;code&gt;file://&lt;/code&gt; URL from &lt;code&gt;LIVE_XML&lt;/code&gt; in our script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;rss&lt;/span&gt; &lt;span class="na"&gt;xmlns:atom=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2005/Atom"&lt;/span&gt; &lt;span class="na"&gt;xmlns:media=&lt;/span&gt;&lt;span class="s"&gt;"http://search.yahoo.com/mrss/"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;channel&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Eyevinn Live Example Streams&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;link&amp;gt;&lt;/span&gt;https://www.eyevinntechnology.se/&lt;span class="nt"&gt;&amp;lt;/link&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Contains live streams generated by Eyevinn Technology&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;atom:link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"http://ENTER_FEED_URL_HERE_IF_UPLOADING.xml"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"self"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Stockholm Office Weather&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;A live RTSP stream of the weather otuside Eyevinn's Stockholm office&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;guid&lt;/span&gt; &lt;span class="na"&gt;isPermaLink=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8&lt;span class="nt"&gt;&amp;lt;/guid&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:category&amp;gt;&lt;/span&gt;All&lt;span class="nt"&gt;&amp;lt;/media:category&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:category&amp;gt;&lt;/span&gt;Weather&lt;span class="nt"&gt;&amp;lt;/media:category&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:content&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:title&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"plain"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Stockholm Office Weather&lt;span class="nt"&gt;&amp;lt;/media:title&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:description&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A live RTSP stream of the weather otuside Eyevinn's Stockholm office&lt;span class="nt"&gt;&amp;lt;/media:description&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:thumbnail&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://picsum.photos/id/1056/200/100.jpg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;/media:content&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;HLS SSAI&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;pubDate&amp;gt;&lt;/span&gt;Thu, 12 Oct 2017 00:00:00 -0700&lt;span class="nt"&gt;&amp;lt;/pubDate&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;link&amp;gt;&lt;/span&gt;http://sample-firetv-web-app.s3-website-us-west-2.amazonaws.com/spectators/&lt;span class="nt"&gt;&amp;lt;/link&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;A live stream of virtual racing action, with stitched in ads from Eyevinn's SSAI server&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;guid&lt;/span&gt; &lt;span class="na"&gt;isPermaLink=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;https://edfaeed9c7154a20828a30a26878ade0.mediatailor.eu-west-1.amazonaws.com/v1/master/1b8a07d9a44fe90e52d5698704c72270d177ae74/AdTest/master.m3u8&lt;span class="nt"&gt;&amp;lt;/guid&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:category&amp;gt;&lt;/span&gt;All&lt;span class="nt"&gt;&amp;lt;/media:category&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:category&amp;gt;&lt;/span&gt;Racing&lt;span class="nt"&gt;&amp;lt;/media:category&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;media:content&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://edfaeed9c7154a20828a30a26878ade0.mediatailor.eu-west-1.amazonaws.com/v1/master/1b8a07d9a44fe90e52d5698704c72270d177ae74/AdTest/master.m3u8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:title&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"plain"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Spectators&lt;span class="nt"&gt;&amp;lt;/media:title&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:description&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;A live stream of virtual racing action, with stitched in ads from Eyevinn's SSAI server&lt;span class="nt"&gt;&amp;lt;/media:description&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;media:thumbnail&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://picsum.photos/id/1070/200/100.jpg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
         &lt;span class="nt"&gt;&amp;lt;/media:content&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;/channel&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/rss&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also want to support a more plain style of RSS/XML, which could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;feed&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2005/Atom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;livetestcontent.mrss.eyevinn.technology&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Feed for LIVE Test-Content&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;updated&amp;gt;&lt;/span&gt;2022-05-05&lt;span class="nt"&gt;&amp;lt;/updated&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;entry&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;HLS LIVE&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;LIVE_1&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;link&amp;gt;&lt;/span&gt;https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8&lt;span class="nt"&gt;&amp;lt;/link&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;https://picsum.photos/id/1056/200/100.jpg&lt;span class="nt"&gt;&amp;lt;/image&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/entry&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;entry&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;HLS LIVE SSAI&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;LIVE_2&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;link&amp;gt;&lt;/span&gt;https://edfaeed9c7154a20828a30a26878ade0.mediatailor.eu-west-1.amazonaws.com/v1/master/1b8a07d9a44fe90e52d5698704c72270d177ae74/AdTest/master.m3u8&lt;span class="nt"&gt;&amp;lt;/link&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;image&amp;gt;&lt;/span&gt;https://picsum.photos/id/1070/200/100.jpg&lt;span class="nt"&gt;&amp;lt;/image&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/entry&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/feed&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our &lt;code&gt;ContentView&lt;/code&gt;, we add some code to read the value of &lt;code&gt;VOD_XML&lt;/code&gt; and &lt;code&gt;LIVE_XML&lt;/code&gt; from our &lt;code&gt;Info.plist&lt;/code&gt;, and we either use their values or, in the case of a &lt;code&gt;file://&lt;/code&gt; URL, the name of the copy destinations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;vodXml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infoDictionary&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"VOD_XML"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as!&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replacingOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"file://"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="s"&gt;"vodContentCopy"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infoDictionary&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"VOD_XML"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as!&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replacingOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also define a &lt;code&gt;loadData()&lt;/code&gt; function, which we call in an &lt;code&gt;.onAppear()&lt;/code&gt; hook attached to our Navigation View. &lt;code&gt;loadData()&lt;/code&gt; will check the values of &lt;code&gt;vodXml&lt;/code&gt; and &lt;code&gt;liveXml&lt;/code&gt;, and either skip them, load them with a &lt;code&gt;URLSession&lt;/code&gt; or from one of the bundled files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;xmlsIndexed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vodXml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;liveXml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enumerated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xmlsIndexed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;isLive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"http://"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"https://"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Encoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
                        &lt;span class="nf"&gt;parseXML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;filenameInBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ofType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentsOfFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filenameInBundle&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Encoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
                    &lt;span class="nf"&gt;parseXML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;loadData()&lt;/code&gt; calls the function &lt;code&gt;parseXML()&lt;/code&gt;, which should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;parseXML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SWXMLHash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;//For XML feed in correct format (see README)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"feed"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;videoItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VideoItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbnailUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
            &lt;span class="n"&gt;isLive&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;liveVideoItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;videoItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;//For MRSS feed in correct format (see README)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rss"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;videoItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;VideoItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"guid"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"media:content"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
            &lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbnailUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"media:content"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"media:thumbnail"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"media:content"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"media:thumbnail"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
            &lt;span class="n"&gt;isLive&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;liveVideoItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;videoItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videoItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the function, we first call the &lt;code&gt;parse()&lt;/code&gt; function of &lt;a href="https://cocoapods.org/pods/SWXMLHash" rel="noopener noreferrer"&gt;SWXMLHash&lt;/a&gt; on our input, and then iterate over the returned &lt;code&gt;SWXMLHash.XMLIndexer&lt;/code&gt; value. Depending on the format of the input, we'll either iterate over every &lt;code&gt;entry&lt;/code&gt; inside the &lt;code&gt;feed&lt;/code&gt;, or every &lt;code&gt;item&lt;/code&gt; inside the &lt;code&gt;channel&lt;/code&gt;; create an &lt;code&gt;VideoItem&lt;/code&gt; object for each, and append it to either our &lt;code&gt;liveVideoItems&lt;/code&gt; or &lt;code&gt;videoItems&lt;/code&gt; array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;And with that, we should have a working tvOS SwiftUI app! &lt;/p&gt;

&lt;p&gt;If you found something to be unclear, or you've gotten stuck, I would once again encourage you to &lt;a href="https://github.com/Eyevinn/tvos-swift-example-app" rel="noopener noreferrer"&gt;clone the project&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I would also encourage you to check out the links below, which I found very helpful while working on this project!&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful resources:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medium.com/cracking-swift/parsing-remote-xml-using-swift-bfa9701fff84" rel="noopener noreferrer"&gt;Parsing XML Using Swift&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.raywenderlich.com/25816315-using-swift-scripts-with-xcode" rel="noopener noreferrer"&gt;Swift Build Scripts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects" rel="noopener noreferrer"&gt;ObservedObject&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-play-movies-with-videoplayer" rel="noopener noreferrer"&gt;VideoPlayer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tvos</category>
      <category>swiftui</category>
      <category>tutorial</category>
      <category>swift</category>
    </item>
    <item>
      <title>Creating a video player web component</title>
      <dc:creator>Sebastian Ljungman</dc:creator>
      <pubDate>Thu, 24 Feb 2022 14:35:33 +0000</pubDate>
      <link>https://dev.to/video/creating-a-video-player-web-component-3lgg</link>
      <guid>https://dev.to/video/creating-a-video-player-web-component-3lgg</guid>
      <description>&lt;p&gt;Eyevinn has an &lt;a href="https://github.com/Eyevinn/web-player" rel="noopener noreferrer"&gt;open-source web player&lt;/a&gt; that can play both HLS and MPEG-DASH streams, automatically selecting the appropriate library for either format.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8ffo7lqb81k2x2ya0rf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8ffo7lqb81k2x2ya0rf.png" alt="Eyevinn Webplayer page"&gt;&lt;/a&gt;&lt;br&gt;
You can check it out &lt;a href="https://web.player.eyevinn.technology/" rel="noopener noreferrer"&gt;here&lt;/a&gt;! It's simple to use: Just enter a manifest URI and press "LOAD" (or use one of the preset buttons). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsvbovfpzhvoettccen6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsvbovfpzhvoettccen6.png" alt="Eyevinn Webplayer video loaded"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Voilà! If we want to share a link to the page with our manifest URI of choice, we can use the "SHARE" button. &lt;/p&gt;

&lt;p&gt;But what if we want to embed the player on our own web page? Then we can click the "EMBED" button to display a code snippet. We copy this snippet, paste it into our HTML file, and we should see an &lt;code&gt;eyevinn-video&lt;/code&gt; element on our webpage.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmczhkil1vqq2w0zqog9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmczhkil1vqq2w0zqog9.png" alt="Web component snippet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does this work? It's thanks to the use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components" rel="noopener noreferrer"&gt;web components&lt;/a&gt;. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/javascript"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@eyevinn/web-player-component@0.1.1/dist/web-player.component.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;eyevinn-video&lt;/span&gt; &lt;span class="na"&gt;source=&lt;/span&gt;&lt;span class="s"&gt;"https://f53accc45b7aded64ed8085068f31881.egress.mediapackage-vod.eu-north-1.amazonaws.com/out/v1/1c63bf88e2664639a6c293b4d055e6bb/ade303f83e8444d69b7658f988abb054/2a647c0cf9b7409598770b9f11799178/manifest.m3u8"&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&amp;lt;/eyevinn-video&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The snippet consists of two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A script tag containing the web component code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An &lt;code&gt;eyevinn-video&lt;/code&gt; custom element&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's have a look at the code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;WebPlayer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@eyevinn/web-player-core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderEyevinnSkin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@eyevinn/web-player-eyevinn-skin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@eyevinn/web-player-eyevinn-skin/dist/index.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerComponent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;observedAttributes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starttime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;muted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;autoplay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Call constructor of HTMLElement&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;//Attach shadow DOM&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shadowRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;//Create style and attach to shadow DOM&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;styleTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;styleTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;styleTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;//Create wrapper and attach to shadow DOM&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;//Create video element and attach to shadow DOM&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;//Init player and skin&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebPlayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;renderEyevinnSkin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;castAppId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;attributeChangedCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starttime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starttime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;autoplay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid source was provided to &amp;lt;eyevinn-video&amp;gt; element&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;muted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;muted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setupEventProxy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;disconnectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;setupEventProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;//Register custom element&lt;/span&gt;
&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eyevinn-video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PlayerComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;First off, we need to import the necessary libraries. We then create a custom class for our web component, &lt;code&gt;PlayerComponent&lt;/code&gt;, which extends the basic &lt;code&gt;HTMLElement&lt;/code&gt; class. The class contains several &lt;code&gt;observedAttributes&lt;/code&gt;; as the name implies, these are the attributes of the custom element that we want to observe.&lt;/p&gt;

&lt;p&gt;When an &lt;code&gt;observedAttribute&lt;/code&gt; is changed, the &lt;code&gt;attributeChangedCallback&lt;/code&gt; function is triggered. The function has the properties &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;oldValue&lt;/code&gt; and &lt;code&gt;newValue&lt;/code&gt;, and we use the the &lt;code&gt;name&lt;/code&gt; property to determine which code to run. For example, when &lt;code&gt;name === 'source'&lt;/code&gt;, this means that the source attribute of the element has been changed. In that case we want to load the new manifest URI, and apply other attributes if present, such as &lt;code&gt;starttime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As for the constructor, it's important to first call the constructor of the super class &lt;code&gt;HTMLElement&lt;/code&gt;, to give our custom element its basic functionality. And rather than appending the elements inside our class to the DOM directly, we're using a shadow DOM: This encapsulates code inside our web component, ensuring that it doesn't affect anything outside it. &lt;/p&gt;

&lt;p&gt;Besides &lt;code&gt;attributeChangedCallback&lt;/code&gt;, we also have access to other callback functions, including &lt;code&gt;connectedCallback&lt;/code&gt; and &lt;code&gt;disconnectedCallback&lt;/code&gt;, which are run when the component is added to or removed from the DOM. In our case, we want to initiate an event proxy with our &lt;code&gt;setupEventProxy&lt;/code&gt; function when the component is added, and reset the player when the component is removed.&lt;/p&gt;

&lt;p&gt;Finally, in order for our custom element to be used in the regular DOM, we need to register it with &lt;code&gt;customElements.define&lt;/code&gt;. This will allow us to add our &lt;code&gt;PlayerComponent&lt;/code&gt; in our HTML under the name &lt;code&gt;eyevinn-video&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it! We can now include as many &lt;code&gt;eyevinn-video&lt;/code&gt; elements as we want on our webpage. We can also add our own style to the element with CSS. Keep in mind that for the super class &lt;code&gt;HTMLElement&lt;/code&gt;, the default value of the &lt;code&gt;display&lt;/code&gt; property is &lt;strong&gt;&lt;code&gt;inline&lt;/code&gt;&lt;/strong&gt;. In the example below, an HLS stream and an MPEG-DASH stream are playing on the same webpage, both using our web player component. The attributes &lt;code&gt;starttime&lt;/code&gt;, &lt;code&gt;muted&lt;/code&gt; and &lt;code&gt;autoplay&lt;/code&gt; were set for both videos, resulting in automatic, muted (required by browsers for autoplay) playback starting at the specified times.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzdax1oz2tz0odbc13ui8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzdax1oz2tz0odbc13ui8.png" alt="Web components with CSS"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might be wondering why we should go through the effort of making our own web components, instead of using iFrame: iFrames have been around forever, and are still the most common way of embedding external HTML on webpages. They are also supported by older browsers.&lt;/p&gt;

&lt;p&gt;Web components, however, give greater control over what parts to include, and may offer &lt;a href="https://medium.com/bbc-design-engineering/goodbye-iframes-6c84a651e137" rel="noopener noreferrer"&gt;both search engine optimization and performance benefits&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
