jwplayer / ott-web-app

Reference implementation for JWP-powered apps
Apache License 2.0
70 stars 52 forks source link

Electronic Programming Guide (EPG) #24

Closed marcovandeveen closed 2 years ago

marcovandeveen commented 2 years ago

Goal

Visualize live channel schedules

Scope

Must have

Should have

Nice-to-have

Out-of-scope

UI Design

Earlier designs:

EPG UI component

https://planby.netlify.app/

JW Admin Workflow

Creating Channels

  1. Register a 24x7 channels as media items in JW
    • Add custom parameter called scheduleUrl for each channel
    • Add custom parameter called contentType with value LiveChannel, which the web app will use to
      • open a channel-specific page when the media item gets opened from somewhere in the web app
      • show a live tag
  2. Add a playlist for your live channels
    • Add custom parameter contentType as Live, which the web app will use to open the EPG page
  3. Register your playlist as a library in the app config, so that it appears in the menu

Managing EPG

Live Stream Chapter Point Management in Broadcast Live

image

Runtime Interfaces

Channel schedule data

The channel schedule data format is from Broadcast Live Chapter Points

[{{
    "id": "6844d9d3-a402-5942-755d-38e6d163e820",
    "eventId": "20001f9e-b24d-4c46-b600-c6c057bb0758", --> ChannelID
    "parentId": null,
    "referenceId": "reference-id",
    "title": "Example Chapter Point",
    "tag": "tag",
    "startTime": "2018-05-04T09:50:10Z",
    "endTime": "2018-05-04T09:54:20Z",
    "chapterPointCustomProperties": [],
    "children": []
}

Broadcast Live Stream Interface

-Admin https://admin.controlhub.jw.poc8.vualto.com/ (Access through mvandeveen@jwplayer.com)

Broadcast Live Stream DVR / Catchup Config

Default: DVR 5min, Catchup 8 hours: image

Important notes:

JW / Broadcast Live Offering

Reference

Alternative data formats considered

Examples EPGs (reference)

marcovandeveen commented 2 years ago

Fox.com inspiration

image

Same replicated with Planby:

image
ChristiaanScheermeijer commented 2 years ago

@marcovandeveen @dbudzins @AntonLantukh

EPG implementation questions and ideas

The EPG feature will be a new integration and is configured in a new integration block. The current plan is to allow something like this in the configuration:

config.json

{
  "integrations": {
    "epg": {
      "provider": "jw", // defaults to jw, but allow to add new providers as well
      "scheduleUrl": "https://epg/url" // URL to fetch the EPG schedule
    }
  }
}

I think all integrations should be abstracted out of the UI layer. An integration should have a generic interface for its topic and should not expose implementation details. This is the reason why it currently is hard to replace Cleeng for example.

!!Heads-up, off-topic!!

While I mention the Cleeng integration, a different problem comes to mind with the current configuration. In the new configuration, cleeng is used as an implementation. In order to make this more generic, I think we should change the config to use a generic integration and configure that to use Cleeng as a provider. Following the approach from above.

{
  "integrations": {
    "auth": {
      "provider": "cleeng",  // could be a separate SSO provider combined with Cleeng for subscriptions
      "publisherId": "123456",
      "useSandbox": true
    },
    "subscriptions": {
      "provider": "cleeng",
      "useSandbox": true,
      "monthlyOffer": "S12345678",
      "yearlyOffer": "S23456789"
    }
  }
}

The above does have an issue since it still contains Cleeng-specific properties. But I guess this should be handled in the implementation correctly.

Another question that comes to mind is how to deal with React Query and Zustand state. That's a problem for later I guess...

!!Back to the topic!!

Integration

I'm thinking to create the following directory/file structure:



PS I'm still doubting if we should also add all EPG-related components (and screens) to the EPG integration directory.

The epg/index.ts file is the only part the UI can actually talk to and get data from. For example, it could expose the following methods:

interface EPGSchedule {
  channels: [EPGChannel];
}

interface EPGProvider {
  getSchedule: (config: EPGConfig) => Promise<EPGSchedule>;
}

type EPGConfig = {
  provider: string;
  scheduleUrl: string;
  [key: string]: unknown; // implementation properties
}

// integration interface
interface Integration {
  // is it enabled?
  enabled: boolean;
  // the integration config
  config?: { [key: string]: unknown };
  // initialize integration
  initialize: (config: { [key: string]: unknown }) => Promise<void>;
}

class EPGIntegration implements Integration {
  enabled: boolean = false;
  config?: EPGConfig;
  provider?: EPGProvider;

  async initialize (config: { [key: string]: unknown }) {
    if (!config.provider) throw new Error('EPG `provider` not given');
    if (!config.scheduleUrl) throw new Error('EPG `scheduleUrl` not given');

    this.config = config as EPGConfig; // type check first

    // Lazy load provider (or map it)
    this.provider = await import(`./providers/${config.provider}`);
    this.enabled = true;
  }

  async getSchedule () {
    if (!this.config || !this.provider) throw new Error('We\'re missing some things...');

    return this.provider.getSchedule(this.config)
  }
}

UI

There are a few UI changes and additions needed. The first change is a new screen that a user can use to see the full EPG. The easiest way is to add a new screen (perhaps just EPG.tsx or Schedule.tsx) and add a conditional route for this screen when the EPG integration is enabled.

For example:

        <Route path="/o/about" component={About} />
        {epg.enabled && <Route path="/epg" component={EPG} />}

An alternative approach would be to have the integration expose screens. This way all EPG-related code will be kept in the EPG integration directory.

        <Route path="/o/about" component={About} />
        {epg.getRoutes().map(({ path, component }) => <Route path={path} component={component} key={path} /> )}

Or using an IntegrationService (à la Service Container) which combines the routes from all integrations.

Although, the latter can be considered to be a bit over-engineered.

The second change is to allow a menu item to point to the schedule screen. I also see two options here.

1) We add the menu item automatically when enabling the EPG (perhaps with a config param) 2) We allow adding menu items via the menu property pointing to internal routes

The third change is in the Movie screen where the user can start watching the live stream or DVR program. I'm not sure yet if we want a dedicated screen for live content since it does contain different logic. For example, it doesn't use the platform APIs and is mostly driven by the EPG data. It also needs different URL parameters to identify the EPG channel and program when watching DVR.

Let me know what you think! 😄

marcovandeveen commented 2 years ago

@ChristiaanScheermeijer Wow. Nice to see this preparation

I like the decoupling of the EPG dataformat from the UI.

I was thinking of the following workflow, to allow channel management from the dashboard:

  1. Register your 24x7 channels as media items in JW
    • Add custom parameter called scheduleUrl for each channel
    • Add custom parameter called contentType with value LiveChannel, which the web app will use to
      • open a channel-specific page when the media item gets opened from somewhere in the web app
      • show a live tag
  2. Add a playlist for your live channels
    • Add custom parameter contentType as Live, which the web app will use to open the EPG page
  3. Register your playlist as a library in the app config, so that it appears in the menu

The channel data format:

[{
    startTime: "2022-05-24T23:50:00",
    endTime: "2022-05-25T00:55:00",
    title: "The Fairly OddParents",
    description: "Follow Vivian Turner, and her new stepbrother, Roy Ragland, as they navigate life in Dimmsdale"
    image: "https://www.themoviedb.org/t/p/w1066_and_h600_bestv2/image.jpg" 
  }]

Some additional behavior

ChristiaanScheermeijer commented 2 years ago

Thanks @marcovandeveen!

Right, thank you for the additional info. We should indeed consider making it easily configurable via the dashboard.

But this actually means that it won't be an optional integration. We can still make the provider configurable, but it should be enabled by default so that publishers only have to add the live channel in the dashboard without changing the configuration. Do you agree?

Some new questions:

1) Do we want a separate URL for the EPG page or use the playlist page with a Router like we're doing in series? 2) The new config dashboard won't allow changing the content.type. This means that we would need to request all menu items in order to obtain the contentType. 3) The channel program data looks clear

Regarding the additional behavior

1) Sounds good, should we show that as the title on the live stream page as well? Currently, it shows the current program title 2) Do you mean that the data is fixed to a specific date and not updated dynamically, e.g. the frontend ignores the date and only uses the time on today's date? 3) How is the "demo mode" activated?

marcovandeveen commented 2 years ago

But this actually means that it won't be an optional integration.

  • You are right, keeping the web app core lightweight and modular is important. I expect about 10-20% of the implementations will use the module. Those with a 24x7 channel. This might grow once we 24x7 channels become easier accessible from he dashboard
  • I suggest making it a module that is dynamically loaded based on config or even at build time.
  • I'll make sure the setup step is clearly documented in the JW documentation. Similar to the setup steps for the Cleeng integration
  1. Do we want a separate URL for the EPG page or use the playlist page with a Router like we're doing in series?
  2. The new config dashboard won't allow changing the content.type. This means that we would need to request all menu items in order to obtain the contentType.
    • I suggest we align with the plans for #54 and #89. This is part of our 22Q3 plan.
    • As part of #89 we need contentType-screen-mapping mechanism. Probably in the app config.

Should we show that as the title on the live stream page as well? Currently, it shows the current program title

  • Make sense, let's discuss that with Stephan

How is the "demo mode" activated?

  • I suggest an optional custom parameter scheduleDataFormat, that can be used to indicate which dataformat is used. Giving it value 'demo' ensures the system automatically overwrites the dates from scheduleUrl to today and retrieve a dummy schedule in case there is no scheduleUrl set
dbudzins commented 2 years ago

@ChristiaanScheermeijer thank you for putting so much thought into this. I totally agree that the 'provider' approach is the way to move going forward. I think we can probably remove 1 layer of abstraction here and just have integrations or providers (naming isn't super important) but not both. For initialization we can try something like one of the below to abstract the dependencies (I don't have any experience with them, but spent a few minutes Googling react DI / service locator libraries):

This would mean a src/providers directory, where all providers will live. If necessary we can nest them in subdirectories (src/providers/epg, src/providers/auth, etc) though I'm not sure with Cleeng for example whether it makes sense to split the current cleeng into a bunch of different classes for each thing it does. I don't know if it makes more sense to have specific config sections for each expected provider or just allow a generic list of providers.

I would also suggest we start out with the screens and UI components in the existing directories as part of the full app with conditionals to hide it if epg isn't enabled, instead of grouping them as integrations. Maybe there will be a point where adding an integration brings a bunch of UI with it, but right now it's all pretty entangled and we would need to abstract a lot for all of the different things to work right. If we have a bunch of time left in the sprint, we can try to tackle this because it would be a cool flexibility for other stuff, but I think EPG is 'core' enough that no one will question having it in the main app.

AntonLantukh commented 2 years ago

@dbudzins @ChristiaanScheermeijer I agree that EPG seems to be a core thing but the implementation itself could differ. For now it seems to be okay just to isolate the EPG integration (EPGIntegration) like Christiaan proposed and to use the component we store in the components folder.

The task to organise pluggable / modularized structure seems to be a really interesting one. I have many ideas and will do my best to propose possible solutions. In addition, that may also be useful to configure the app via local UI (like Vui for example). However we don't have full understanding of further steps now, so let's try to stick to the current logic.

For menu items I would suggest not to include them automatically as it may seem to be reasonable to have just a shelf and to use menu for other things. I think we can discuss whether we can add live type in include a menu item as an EPG one.

ChristiaanScheermeijer commented 2 years ago

Thanks @marcovandeveen @dbudzins and @AntonLantukh for your feedback.


@marcovandeveen

  • You are right, keeping the web app core lightweight and modular is important. I expect about 10-20% of the implementations will use the module. Those with a 24x7 channel. This might grow once we 24x7 channels become easier accessible from he dashboard
  • I suggest making it a module that is dynamically loaded based on config or even at build time.
  • I'll make sure the setup step is clearly documented in the JW documentation. Similar to the setup steps for the Cleeng integration

Sounds good!

Yes, preferably will do so, do you want us to propose implementation solutions for this or will you (@dbudzins @AntonLantukh) suggest ideas for this?

  • I suggest an optional custom parameter scheduleDataFormat, that can be used to indicate which dataformat is used. Giving it value 'demo' ensures the system automatically overwrites the dates from scheduleUrl to today and retrieve a dummy schedule in case there is no scheduleUrl set

We can do that. This parameter is defined in the live media item custom properties, right? By default, we expect an ISO8601 date format, but this can be overridden with a date-fns format string. We currently don't depend on the date-fns library, but the planby library does.


@dbudzins

I totally agree. I've checked out the libraries and that looks interesting. Are you suggesting that we have a "service" interface and potentially multiple implementation services?

For example:

interface EPGInterface { }

@Service()
class JwEPGService implements EPGInterface {}

@Service()
class OtherEPGService implements EPGInterface {}

And dynamically load the ServiceContainer based on the config?

<ServiceContainer 
  services={[{
    provide: 'epg',
    useClass: integrations.epg.provider === 'other' ? OtherEPGService : JwEPGService,
}]}>
  {children}
</ServiceContainer>

I like that idea as well. Do you think it will be realistic to work on this next sprint or do you want some more time to discuss and play with the idea first?


@AntonLantukh

Yes, that actually answers the question from above. If we stick to the current logic, I then probably create an EPG service, hook and component. Do you agree?

For menu items I would suggest not to include them automatically as it may seem to be reasonable to have just a shelf and to use menu for other things. I think we can discuss whether we can add live type in include a menu item as an EPG one.

We will use the current logic for this. The configured playlist will determine which screen is used. We've talked about configurable screens based on the contentType. I'm not so sure if we want to do dynamic URLs a.t.m. This makes it a lot harder to implement.

I think we can now create router screens for the playlist, series and movie URLs and present the configured contentType or default layout for the screen.

For example:

config.json

{
  "layout": {
    "movie": {
      "default": "Movie",
    },
    "series": {
      "default": "Series"
    },
    "playlist": {
      "default": "Playlist",
      "Live": "EPG" // <-- epg when `playlist.contentType` === 'Live'
    },
  }
}
const PlaylistRouter = (...) => {
  const playlist = usePlaylist(...)

  const defaultLayout = config.layout.playlist.default;
  const layout = config.layout.playlist[playlist.contentType];

  // lazy load layout component

  return <Layout playlist={playlist} />
};
dbudzins commented 2 years ago

Yes that's what I had in mind for the services (or providers or modules or whatever we decide to call them.) To achieve the 'lightweight' core that @marcovandeveen is looking for though, we would need to be able to do everything at compile time and/or serve each module's code as a separate js file that wouldn't be downloaded if not used. I think I like the latter best if possible, because it prevents someone from creating a build that won't work if they exclude something and then adjust the config and try to use it later. Definitely let's leave this as lower priority than the EPG work itself. If there's time at the end of the sprint we can discuss more or try to figure out a POC.

Service, hook, and component sound like the likely main building blocks. From my standpoint, I would just suggest the component lives with the rest of the UI code, not abstracted to be part of the EPG service. We may decide we want modular UI in the future, but I think if we do, it will be safest to make the UI and service modules completely independent.

ChristiaanScheermeijer commented 2 years ago

Exactly, since it essentially will be used for all integrations we need to work this out more. I see what you mean by "provider" in inversify terms. It lets you load a service asynchronously which results in happy bundle sizes.

Sounds like a plan. We will try creating as "modular" as possible. I think the biggest bundle change will come from the planby module. But if we lazy load the EPG component, planby shouldn't be included in the main bundle.