Video Feed Performance

So we have this code for a video feed loading videos into the socket:

def handle_event("video_visible", %{"id" => activity_id, "videoUrl" => video_url}, socket) do
    socket = assign(socket, :current_video_url, video_url)

    {:noreply,
     push_patch(socket,
       to: "#{socket.assigns.globals.request_uri.path}?activity_id=#{activity_id}"
     )}
  end

  def handle_event("toggle_mute", _params, %{assigns: %{muted: muted}} = socket),
    do: {:noreply, assign(socket, muted: !muted)}

  # Called from inside video_tile function
  def handle_event("show_activity_modal", %{"id" => id}, socket) do
    {:ok, activity} = @data.get_activity_by_id(id)
    socket = assign(socket, :featured_activity, activity)

    {:noreply, push_patch(socket, to: "#{socket.assigns.globals.request_uri.path}?activity_id=#{id}")}
  end

  @impl Phoenix.LiveView
  # Called from inside app.js for infinite scroll
  def handle_event("load_more_videos", _params, socket) do
    {:noreply, load_videos(socket)}
  end

  @impl Phoenix.LiveView
  def handle_params(%{"activity_id" => activity_id}, _uri, socket) do
    {:ok, activity} = @data.get_activity_by_id(activity_id)
    socket = assign(socket, :page_title, Scorpion.PageTitles.activity(activity))
    {:noreply, socket}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end

  defp load_videos(socket) do
    socket = assign(socket, :current_page, socket.assigns.current_page + 1)
    deep_linked_activity = socket.assigns.featured_activity

    {:ok, search} =
      SearchParamsUtil.search(socket.assigns.search_params,
        page: socket.assigns.current_page,
        page_size: 5
      )

    entries =
      if deep_linked_activity do
        [
          deep_linked_activity
          | Enum.reject(search.results.entries, &(&1.id == deep_linked_activity.id))
        ]
      else
        search.results.entries
      end

    videos =
      Enum.map(entries, fn entry ->
        %Video{
          id: entry.id,
          url: entry.videos |> Enum.random() |> Map.fetch!(:url_mp4),
          activity: entry,
          liked: entry.id in socket.assigns.liked_activity_ids,
          poster: List.first(entry.image_urls)
        }
      end)

    socket = assign(socket, :videos, Enum.concat(socket.assigns.videos, videos))
    socket = assign(socket, :region, search.region)

    if search.region && Enum.empty?(videos) do
      push_navigate(socket, to: region_path(search.region, true))
    else
      socket
    end
  end

and this javascript

Hooks.VideoDeepLinkOnScroll = {
  mounted () {
    const $this = this
    var observer = new IntersectionObserver(onIntersection, { threshold: 0.5 })
    const id = this.el.getAttribute('data-id')
    const videoUrl = this.el.getAttribute('data-url')

    const videoPlaytimeMap = {}
    const currentVideo = null
    function onIntersection (entries) {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          window.Peek.Analytics.track('Video Viewed', {
            activity_id: id,
            activity_name: $this.el.getAttribute('data-name'),
            url: videoUrl
          })
          $this.pushEvent('video_visible', { id, videoUrl })
          firstSource = document.getElementsByTagName('source')[0]
          if (firstSource) {
            firstSource.setAttribute('src', videoUrl)
            document.getElementsByTagName('video')[0].load()
          }
        }
      })
    }
    observer.observe(this.el)
  }
}

and this html

<ActivityComponents.video_overlay :if={@current_video_url} url={@current_video_url} />
    <div class="h-screen overflow-y-scroll snap-y snap-mandatory relative inset-x-0" phx-hook="StopStartVideo" id="video-wrap" phx-update="append">
      <%= for {video, index} <- Enum.with_index(@videos) do %>
        <div
          phx-hook="VideoDeepLinkOnScroll"
          data-name={video.activity.name}
          data-url={video.url}
          data-id={video.id}
          data-poster={video.poster}
          class="relative mx-auto max-w-full sm:max-w-[700px]"
          id={"video-wrapper-#{video.id}"}
        >
          <div class="h-screen object-cover w-full snap-start"><ActivityComponents.video_tile globals={@globals} video={video} index={index} /></div>
        </div>
      <% end %>
    </div>

And so my question is… is there a way to refactor or improve performance for this? The main thing being: our poster image is a random shot from somewhere in the middle of the video. I tried to use javascript to draw a “canvas” from the first frame of the video, but it doesn’t like the format of the video not being html5. I’m wondering if that’s because it’s loaded into the socket from a url? Anyway, it’s mostly the flash of the poster being different that is making the performance janky, and I’m wondering if there’s a way around this. Thank you!

Hi @charlottemoche when you say improve, can you speak to the baseline performance you are observing? What performance issues do you see?

1 Like

Good question! It’s mostly that I’m wondering how the elements are being rendered if they are not registered as HTML5 elements. Are we loading them in the most efficient way? Because the performance might be pretty quick, but it’s just that I want to draw the canvas which would help it look more performant even if it doesn’t need to be or isn’t actually more performant. And in order to get that video poster canvas, it has to be an HTML5 element. Does this make sense? Not sure if I’m explaining it well.

We need a poster because we’re doing a tiktok style swiping video feed, and the videos aren’t loading fast enough. So you see our dark gray background… then a video.poster… and then the video. So it looks kind of messy

Ahh. I am not a front end expert here so perhaps someone else can weigh in, but my impression is that one way to do this is to essentially preload N videos in the actual dom, but have all but the current one hidden.

It’s hard for me to tell, but it looks like you have exactly one video at a time loaded. The main idea of the UX optimization is to also load a certain number of videos beyond what the user can see hidden via CSS, and then unhide them as they are swiped.

That is actually how we had it functioning before re: hiding with CSS! We are, however, preloading 5 videos into the socket, and then swapping out the url one at a time because of a mute button issue we were having :frowning: so the issue persists regardless I think…