muxinc / media-chrome

Custom elements (web components) for making audio and video player controls that look great in your website or app.
https://media-chrome.org
MIT License
1.72k stars 70 forks source link

UI for linear advertisements #34

Closed littlespex closed 1 year ago

littlespex commented 3 years ago

Requirements

Provide a set of components for displaying and controlling linear advertisements. The controls layer would provide functionality to:

Proposal 1

Create a <media-ad-controller> element which bootstraps the ad SDK and act as a wrapper element for a separate set of ad controls. The ad ui would use the standard media-* controls, i.e. buttons like play and fullscreen, and the <media-ad-controller> would intercept the MEDIA_X_REQUEST events and perform any ad specific logic.

<media-controller>
  <video slot="media"></video>
  <media-control-bar>
    <media-play-button />
    <media-mute-button />
    <media-volume-range />
    <media-time-range />
    <media-pip-button />
    <media-fullscreen-button />
  </media-control-bar>
  <media-ad-controller>
    <media-ad-info />
    <media-control-bar>
      <media-play-button />
      <media-mute-button />
      <media-volume-range />
      <media-time-range />
      <media-pip-button />
      <media-fullscreen-button />
    </media-control-bar>
  </media-ad-controller>
</media-controller>

Pros

Cons

Proposal 2

Similar to the first proposal but with the <media-ad-controller> wrapping the entire UI.

<media-controller>
  <media-ad-controller>
    <video slot="media"></video>
    <media-control-bar>
      <media-play-button />
      <media-mute-button />
      <media-volume-range />
      <media-time-range />
      <media-pip-button />
      <media-fullscreen-button />
    </media-control-bar>
  </media-ad-controller>
</media-controller>

Pros

Cons

Other considerations

The current set of web components revolve around a stable public API: HTMLMediaElement. There is no similar standard for ad playback. When using a raw playback engine likehls.js or dash.js the ad implementation would need to be done separately. This could easily be encapsulated using <media-ad-controller> as a base class, and creating a ad-vendor specific element such as <ima-ad-controller>. For full featured players like Shaka and Video.js, ad APIs are provided directly by the SDK, or via a plugin. In those cases there would need to be a element that translates the player specific APIs into a normalized API that <media-ad-controller> could use. Similar to the way <youtube-video> provides an API to mirror HTMLMediaElement.

mmcc commented 3 years ago

We talked about this in Video Dev Slack, but a 3rd proposal is to mix the two a bit more. I like the higher level goals in Proposal 1, but I agree that having sibling elements with this much of an effect on each other feels a little awkward. My 2 cents would be that we go towards something like:

<media-controller>
  <media-ad-controller>
    <video slot="media"></video>
    <media-control-bar>
      <media-play-button />
      <media-mute-button />
      <media-volume-range />
      <media-time-range />
      <media-pip-button />
      <media-fullscreen-button />
    </media-control-bar>
    <media-ad-control-bar>
      <media-play-button />
      <media-mute-button />
      <media-volume-range />
      <media-time-range />
      <media-pip-button />
      <media-fullscreen-button />
    </media-ad-control-bar>
  </media-ad-controller>
</media-controller>

The only special feature of the <media-ad-control-bar> would be that it would namespace the underlying children's events as they bubble up, allowing the <media-ad-controller> to behave in the same way as proposed, but with the additional benefit gained from fully wrapping the underlying media element.

Thoughts?

heff commented 3 years ago

I'm liking the general idea and conversation direction here. 👍

With custom elements, can a slotted element (media) be inside a child or does it need to be a direct descendent? I couldn't find a quick answer, but that's obviously a key detail here. Hypothetically, we could have media-ad-controller wrap media-controller if that makes it possible and simplifies some logic.

Ads...so much to wrap your brain around. Does this approach apply equally to

Does it matter? Are all of those still a concern?

heff commented 3 years ago

It doesn't appear slots in children work today. So in the first few examples, either media-ad-controller would need to live in the media slot itself (duplicating the API of a media element) or pass the media to the media-controller some other way.

But that's pushing me to want to explore wrapping media-controller with media-ad-controller.

<media-ad-controller>
  <media-controller>
    <video slot="media"></video>
    <media-control-bar>...</media-control-bar>
  </media-controller>
  <!-- ad control bar -->
  <media-control-bar>...</media-control-bar>
</media-ad-controller>

What are the downsides of that? Feels a little odd to not have media-controller be the consistent top-level element, but I'm not totally sure of the specific problems that creates.

An upside is media-controller and all of the other core elements would have less need to expand to be ad-aware.

mmcc commented 3 years ago

But wouldn't that approach make the event bubbling stuff a bit weird? What if we did something similar to what I proposed for media-ad-control-bar for media-controller? Just having that base element extend media-controller feels like it could both simplify the markup and the component itself.

heff commented 3 years ago

But wouldn't that approach make the event bubbling stuff a bit weird?

Can you say more about weird?

What if we did something similar to what I proposed for media-ad-control-bar for media-controller?

Yeah that's definitely an option too. A sub class that bolts on ad functionality to the controller. We'd still need to distinguish between control bars in that case.

mmcc commented 3 years ago

Can you say more about weird?

Doesn't it feel weird that a parent element would affect the bubbling of events to a child? I'm not even 100% sure how we'd even handle that, but just looking at that markup and thinking of the implication makes me squint a little.

We'd still need to distinguish between control bars in that case.

Totally, I think I'd still fall back to the special media-ad-control-bar outlined.

heff commented 3 years ago

Doesn't it feel weird that a parent element would affect the bubbling of events to a child? I'm not even 100% sure how we'd even handle that, but just looking at that markup and thinking of the implication makes me squint a little.

I'm probably not seeing the whole picture but I'm not sure why the parent element (ad controller?) would need to affect bubbling. I think the ad controller would listen to the events it needs from media-controller, but wouldn't need to do anything with the other events.

Relationship-wise, it seems like the ad controller needs to know about media events, but the media-controller doesn't need to know about ad events. The media-controller gets controlled by the ad-controller as needed, maybe similarly to how the IMA ad plugins work with the video element. The video element does its thing, and the ad framework works around it.

It's pretty much exactly like when you wrap optimus prime in bigger optimus prime. image

littlespex commented 3 years ago

@heff Good catch with the child slots issue. Proposal 2 will not work as written. The media-ad-controller will need to wrap the entire UI, either by containing the original media-controller as a child, or extending it as a base class.

I'm probably not seeing the whole picture but I'm not sure why the parent element (ad controller?) would need to affect bubbling.

If the ad controller only wraps the ad specific controls (proposal 1), it would be able to intercept the MEDIA_REQUEST_X events fired by its children and perform ad specific logic, and then prevent the events from bubbling up to the standard media-controller.

If the ad controller wraps the entire UI, there would need to be a new set of controls that fire ad specific events so that the ad controller could differentiate between the content actions and ad actions. This wouldn't be too difficult as the new ad controls could simply extend the existing controls and override the event emission with the new ad specific events.

All this being said, I think the much bigger problem is: Where does the ad SDK integration live, and how do the web components interact with it.

Let's use Video.js and an example. To get IMA ad playback we might use a plugin such as videojs-ima. This would provide a source for the ad events needed to render the ad ui, adstart, adend, etc, but how would the web components consume those events? The player instance lives in JS, and the events are 'emitter' style events, not DOM events. Would we need to have custom media element components for every player framework that dispatch a set of standard ad DOM events?

heff commented 3 years ago

Got it, I see now. Thanks for clearing that up.

One thing I'm feeling through all of this is media-controller probably needs a set of defined ad events, to at least be able to proxy ad state. But otherwise, I think we might need to break up the different ad situations into their own problem sets, because they might need to be solved differently. Let me try to enumerate them...

Server-side ads

The media element itself needs to be able to emit standardized ad events at minimum, if not other functions like blocking scrubbing, handling clicks on the ad, and sending ad click reports.

Client-side ads

Client-side ads via an ad-supporting media element, e.g. <theo-player> or <video-js>

The media element needs to emit standardized ad events, or it can just handle the ad UI internally (not ideal, but an option).

Client side ads via a custom media-chrome element integration

All the options we've discussed... but new questions

After laying that out...I think ad playback might need to live in the media element, while ad UI can live in media-chrome. Everything gets pretty complex if we try to let the ad integration live in the UI layer. Whereas if we can separate ad playback from ad UI, then media-chrome can still play to its strengths. And we're going to have to support media-els-with-ads either way, unless we're going to force you to use the media-chrome ad integration, and I don't love that.

At its simplest it might look like <video-ima> as a media element that also emits standardized ad events, but doesn't worry about UI. Then media chrome passes state like media-ad-playing, that the UI can be built around.

The downside would be that you can't just easily add ad support at will to any media element like <hls-video> or <youtube-video>. Is there a way to make that easy? Or is that ok, at least for v0 of media-chrome? Am I missing anything else major?

heff commented 3 years ago

If the ad controller wraps the entire UI, there would need to be a new set of controls that fire ad specific events so that the ad controller could differentiate between the content actions and ad actions.

Had more thoughts on this one, incase UI level integrations are still a direction we need to go. I don't know that we would need ad specific UI events here. The ad-controller could add event listeners to media-play-request and other events, and preventDefault to block further actions when needed (similar to how the IMA SDK works). This is a benefits of having every request funneled through the controller now rather than UI elements talking directly to the media element. Then the ad-controller can rely on its own current media-ad-playing state to determine if UI events should apply to the ad or the media.

There does seem to be a key long term implication to consider here, which is whether or not <media-controller> base media states (e.g. media-duration, media-current-time) are updated to reflect the ad state or maintain the primary media's state. For example if media-current-time is updated to reflect the ad's current time, then external elements now need to be "ad-aware". For example an external text transcript that's following along with the video. Or just a captions element. I think the answer is we don't want that.

(this is very complicated... sorry for the all the long exploratory comments)

Pulling from various ideas, here's a proposal...

1. Add first-class ad states to media-controller

<media-controller media-ad-playing media-ad-duration="100" media-ad-current-time="10" ...>

2. media-controller listens for ad-events on the media element, and updates ad state accordingly

<media-controller media-ad-playing>
  <jw-player></jw-player> <!-- media element with internal ads -->
</media-controller>

3a. A UI-level client-side ad integration can wrap media-controller and update its ad state accordingly.

The ad integration element can listen for a media-play-request on media-controller and preventDefault to block media playback and play an ad.

<media-ima-ads>
  <media-controller media-ad-playing>
    <video></video>
  </media-controller>
</media-ima-ads>

3b. A UI-level client-side ad integration could also wrap the media element, taking the the place of the media and exposing the html5 video API.

<media-controller media-ad-playing>
  <ima-video slot="media">
    <hls-video></hls-video>
  <ima-video>
</media-controller>

4a. For all cases, controls could then be built with a special control bar (Matt's idea) that translates events/states

<media-controller>
  <video></video>
  <media-control-bar> <!-- control bar for media -->
    <media-play-button />
  </media-control-bar>
  <media-ad-control-bar> <!-- control bar for ads (special) -->
    <media-play-button />
  </media-ad-control-bar>
</media-controller>

4b. For the media-controller wrapping ad integration case, controls could also optionally live outside the media-controller, and not require a special control bar.

<media-ima-ads>
  <media-controller media-ad-playing>
    <video></video>
    <media-control-bar /> <!-- control bar for media -->
  </media-controller>
  <media-control-bar /> <!-- control bar for ads (not special) -->
</media-ima-ads>

Thoughts on that?

littlespex commented 3 years ago

Thinking about the overall design again with @heff, another approach that doesn't require new elements or DOM hierarchy would be:

The media element (slot="media") encapsulates the content vs ad complexity, leaving the media controls and media-controller as is. The media-play-button fires the "mediapauserequest" event and the media-controller calls this.media.pause(), as it does currently. The custom media element would be responsible determining whether to call pause() on the video tag or the ad manager.

The custom media element would need to fire a new event signaling the start/end of ad playback, an adbreak event with a boolean data payload, or perhaps separate adbreakstart and adbreakend events. The controller would listen for that event and set a boolean attribute on itself so that ad specific CSS rules can be written for the child elements:

<style>
  media-controller[media-ad-break] media-progress-range {
    pointer-events: none;
    background-color: #FFCC33;
  }
</style>
<media-controller media-ad-break>
  <player slot="media" />
  <media-control-bar>
    <media-progress-range />
  </media-control-bar>
</media-controller>

The display of ad data could be handled in a couple of different ways. For a "YouTube style" display, when the ad break starts the custom media element would fire a durationchange event and populate its currentTime and duration properties with ad specific values: Screen Shot 2021-09-24 at 4 25 58 PM

For a "Hulu style" display, the custom media element would need to provide additional pieces of state to allow for a separate rendering of ad time:

It would be the custom media element's responsibility to determine if media-ad-duration represents the duration of an individual ads, or the duration of the entire break. The custom media element would also need to populate media-ad-url for a "Learn More" button / click-thru element.

The last piece would be a single new API method on the custom media element for skipAd().

heff commented 3 years ago

Thanks Casey, this all sounds good. I think we could handle both cases and let the UI dev choose, if we provide the details of the adbreak, and always rely on media-duration/currentTime to represent the current time.

After a mediaadbreakstart event, the media element updates these to represent the current ad.

With then some added ad-specific details

Most cases I think would just choose the simple 1-ad path. It does seem to be a key decision whether we always split the ad metadata from the primary video metadata. Including things like, does media-duration get updated before or after mediaadbreakstart, seeing as it would also fire the 'durationchange` event?

littlespex commented 3 years ago

I think we will have to provide the ad metadata separately. In addition to the time info, metadata about whether the ad is skippable and clickable can vary from ad to ad. Furthermore, for live streams the total number of ads in a break isn't always known at the beginning of the break. This would leave us with the following list of changes needed for a production ready ad UI:

Methods

Custom Media Element

Events

Custom Media Element

Media controls

Attributes

Custom Media Element

Media controls

Slots

We will need at least one new slot, or perhaps a specific custom element, that acts as the container for ad rendering. Most ad SDKs require this element to be passed in to ad manager.

heff commented 3 years ago

I like this.

clickAd() - I like that this is putting it back on the media element to handle the click (and I assume track it). I had assumed it would be saying what to do on a click, like giving a URL to follow. This also makes we wonder if we should be setting up a rule that "the media element should not handle its own clicks". That kind of makes sense since the controller is already handling clicks on the media to toggle play/pause, and you know, we're building the user interface here. But also maybe that's not necessary.

media-ad-mode attr - how would you feel about a name more like media-ads-playing or media-ad-break?

mediaadstart - In our QoE sdks we've used adplay and adplaying, because there's still that difference, and I think that could still be relevant for the UI, like showing a loading spinner.

Here's a question...do we need a mediaadend event, or does the UI just care about the next adplay or adbreakend? I like the completeness of including mediaadend but feels like a good question to ask for the sake of keeping things simple. Maybe the question is, what media-attrs change when mediaadend is fired?

We'll need something additional to know how many ads are in the break, when we can know that. e.g. media-ad-break-ads-count, media-ads-in-break

This is feeling really good. 👍

littlespex commented 3 years ago

I'm open to any naming changes, and I would go with media-ad-break. For mediaadend, I can't think of a UI reason for this event that couldn't be met by adplay or adbreakend. I will update the previous comment with these changes, as well as a note about a new slot for an ad container.