Exploration & Documentation - A simple demo and documentation project exploring the Freewheel client-side library integration with HLS streaming.
This project serves as a learning resource for understanding:
- Freewheel client-side ad insertion
- HLS streaming with adaptive bitrate
- State management patterns
- Building video players with Preact
Note: This is a basic implementation for educational purposes, not production-ready.
- HLS Streaming: Adaptive bitrate streaming via HLS.js
- Freewheel Integration: Client-side ad request and insertion
- Basic Controls: Play/pause and mute/unmute buttons
- Click-to-Play: Manual playback initiation
- Debug Logging: Query parameter
?logfor console output
- Node.js 18+ (install NVM)
- pnpm package manager
- Install dependencies:
pnpm install- Start development server:
pnpm devThe app will be available at http://localhost:5173
pnpm buildOutput will be in the dist directory.
pnpm previewThe project is automatically deployed to GitHub Pages on each push to main.
Update the player configuration in the HTML page or pass it directly:
const playerConfig = {
assetId: "your-asset-id",
manifestUrl: "https://your-hls-manifest.m3u8"
};
const player = new Player();
player.init(playerConfig);The demo page provides controls to manage query parameters:
?log- Enable console debug logging- Click "Toggle Logging" button to add/remove
- Page reloads when toggled
# Enable logging
https://pzanella.github.io/freewheel-html5-video-player/?log
The player uses Zustand for reactive state management:
// Store state
{
type: CONTENT_TYPE, // ADS | VOD
playbackStatus: PLAYBACK_STATUS, // PLAYING | PAUSED
mute: boolean // Mute state
}
// Available actions
togglePlayback() // Toggle between PLAYING/PAUSED
toggleMute() // Toggle mute state
setType(type) // Set content typeThe player includes:
- Playback Button: Play/pause toggle with visual feedback
- Volume Button: Mute/unmute toggle
- Responsive Design: Works on desktop, tablet, and mobile
src/
├── main.ts # Player initialization & orchestration
├── store.ts # Zustand state management
├── logger.ts # Debug logging utility
├── model.ts # TypeScript types & enums
├── emitter.ts # Event emitter base class
├── controls/ # UI components
│ ├── components/
│ │ ├── Icon/ # SVG icon component system
│ │ ├── Playback/ # Play/pause button
│ │ └── Volume/ # Volume button
│ └── index.ts # Controls coordinator
├── ad-content/ # Freewheel ad integration
│ ├── index.ts
│ ├── model.ts
│ └── config.ts
└── media-content/ # HLS video content
├── index.ts
└── model.ts
index.html # Landing page with demo controls
| Component | Version | Purpose |
|---|---|---|
| Preact | 10.27.2 | Lightweight UI framework |
| Zustand | 5.0.8 | State management |
| Vite | 6.2.0 | Build tool & dev server |
| TypeScript | ~5.7.2 | Type safety |
| HLS.js | 1.6.2 | HTTP Live Streaming |
| Tailwind CSS | CDN | Styling |
| pnpm | Latest | Package manager |
// Import Player class
import Player from './path/to/player';
// Configure and initialize
const playerConfig = {
assetId: "your-asset-id",
manifestUrl: "https://your-stream.m3u8"
};
const container = document.getElementById('player-container');
const player = new Player();
player.init(playerConfig);
// Listen to events
player.on('play', () => console.log('Playing'));
player.on('pause', () => console.log('Paused'));The player fires events throughout its lifecycle:
ADS_REQUEST_COMPLETE- Ad request finishedADS_COMPLETE- All ads playedVIDEO_LOADEDMETADATA- Video metadata loadedVIDEO_PLAYING- Video started playingPLAYBACK_STATUS_CHANGED- Play/pause state changed
Exposed as:
window.tv.freewheel.SDK.EVENT_REQUEST_INITIATEDFired when the ad request initiated. The following object returns:
{
type: "onRequestInitiated",
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_REQUEST_COMPLETEFired when the ad request completed, the event triggered after context.submitRequest(); called.
The following object returns:
{
type: "onRequestComplete",
success: boolean,
response: {
ads: LIST_OF_ADS,
...
},
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_SLOT_STARTEDFired when the video ad started.
{
type: "onSlotStarted",
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_INITIATEDFired when the ad initiated.
{
type: "adEvent",
subType: "adInitiated",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_PREINIT{
type: "adEvent",
subType: "preInit",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_BUFFERING_STARTFired when the video ad buffering started.
{
type: "adEvent",
subType: "adBufferingStart",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_BUFFERING_ENDFired when the video ad buffering ended.
{
type: "adEvent",
subType: "adBufferingEnd",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_IMPRESSION{
type: "adEvent",
subType: "defaultImpression",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_FIRST_QUARTILEFired when the 25% video ad completed.
{
type: "adEvent",
subType: "firstQuartile",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_MIDPOINTFired when the 50% video ad completed.
{
type: "adEvent",
subType: "midPoint",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_THIRD_QUARTILEFired when the 75% video ad completed.
{
type: "adEvent",
subType: "thirdQuartile",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_COMPLETEFired when the video ad completed.
{
type: "adEvent",
subType: "complete",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_VOLUME_CHANGEFired when the volume changed.
{
type: "adEvent",
subType: "adVolumeChange",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_MUTEFired when the video ad muted.
{
type: "adEvent",
subType: "_mute",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_UNMUTEFired when the video ad unmuted.
{
type: "adEvent",
subType: "_un-mute",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_PAUSEFired when the video ad paused.
{
type: "adEvent",
subType: "_pause",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_RESUMEFired when the video ad playing after pause event.
{
type: "adEvent",
subType: "_resume",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_AD_IMPRESSION_END{
type: "adEvent",
subType: "adEnd",
adInstance: OBJECT_THAT_DESCRIBE_CREATIVITY,
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_SLOT_ENDED{
type: "onSlotEnded",
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_CONTENT_VIDEO_PAUSE_REQUESTFired before the midroll when the Freewheel library requests control.
{
type: "contentVideoPauseRequest",
slot: SLOT_INSTANCE,
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_CONTENT_VIDEO_PAUSEDFired before the midroll when the Freewheel library pauses the content playback.
{
type: "contentVideoPaused",
target: CONTEXT_INSTANCE,
}Exposed as:
window.tv.freewheel.SDK.EVENT_CONTENT_VIDEO_RESUME_REQUESTFired after the midroll when content playback resumes.
{
type: "contentVideoResumed",
target: CONTEXT_INSTANCE,
}