Notifications
Clear all
Topic starter
Hi,
you might be wondering how to create a media player. Technically, all you need is a simple video element. However, in most cases, additional functionality is required, which is why libraries like Shaka Player or DashJS are commonly used.
This class is designed to help you build a media player. While you could theoretically place everything inside TQTXVideo.
Due to nostalgic reasons I chose to structure it similarly to Delphi, where the player logic and the player window are separated into different classes.
A sample implementation would look like this:
var LVideo = TQTXVideo.Create(self, procedure(AVideoElement: TQTXVideo)
begin
var LPlayer = TQTXBasePlayer.createDefault(AVideoElement);
LPlayer.OpenMedia('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
AVideoElement.Style.width := '100%';
AVideoElement.Style.height := '100%';
LPlayer.OnLoadedData := procedure(Sender: TObject)
begin
LPlayer.Play();
end;
end);
The whole unit is the following one.
unit qtx.mediaplayer;
interface
uses
qtx.sysutils, qtx.classes, qtx.dom.widgets, qtx.dom.types;
type
TQTXVideo = class;
TQTXVideoConstructor = procedure (AVideoElement: TQTXVideo);
TQTXVideo = class(TQTXWidget)
protected
FVideoHandle: THandle;
public
constructor Create(AOwner: TQTXComponent; CB: TQTXVideoConstructor); reintroduce; virtual;
end;
TQTXBasePlayer = class
protected
FVideoHandle: THandle;
FOnLoadedData: TNotifyEvent;
FPoster: String;
procedure InitializePlayer;virtual;
procedure SetPoster(const AValue: String);virtual;
public
constructor Create(const AVideo: TQTXVideo);
procedure Play;virtual;
procedure Stop;virtual;
procedure OpenMedia(AMediaFile: String; ALicenseServer, ACertServer: String = '');virtual;
function IsPlaying: Boolean;
property OnLoadedData: TNotifyEvent read FOnLoadedData write FOnLoadedData;
property Poster: String read FPoster write SetPoster;
class function createDefault(const AVideo: TQTXVideo): TQTXBasePlayer;
end;
TQTXShakaPlayer = class(TQTXBasePlayer)
private
FVideoPlayer: THandle;
protected
procedure InitializePlayer;override;
public
procedure OpenMedia(AMediaFile: String; ALicenseServer, ACertServer: String = '');override;
end;
TQTXShakaUIPlayer = class(TQTXShakaPlayer)
protected
FShakaUI: THandle;
procedure InitializePlayer;override;
procedure SetPoster(const AValue: String);override;
end;
implementation
var FInitialized: Boolean = false;
document external "document": JDocument;
{ TQTXBasePlayer }
class function TQTXBasePlayer.createDefault(const AVideo: TQTXVideo): TQTXBasePlayer;
begin
result := TQTXShakaUIPlayer.create(AVideo);
end;
constructor TQTXBasePlayer.Create(const AVideo: TQTXVideo);
begin
FVideoHandle := AVideo.FVideoHandle;
InitializePlayer;
end;
procedure TQTXBasePlayer.Play;
begin
if FVideoHandle <> nil then
FVideoHandle.play();
end;
procedure TQTXBasePlayer.Stop;
begin
if FVideoHandle <> nil then
FVideoHandle.pause();
end;
procedure TQTXBasePlayer.InitializePlayer;
begin
end;
procedure TQTXBasePlayer.OpenMedia(AMediaFile: String; ALicenseServer, ACertServer:String = '');
begin
if FVideoHandle <> nil then
FVideoHandle.src := AMediaFile;
end;
function TQTXBasePlayer.IsPlaying(): Boolean;
begin
result := (FVideoHandle <> nil) and (not FVideoHandle.paused);
end;
procedure TQTXBasePlayer.SetPoster(const AValue: String);
begin
FPoster := AValue;
end;
{ TQTXShakaPlayer }
procedure TQTXShakaPlayer.InitializePlayer;
procedure doLoadedData();
begin
if assigned(FOnLoadedData) then FOnLoadedData(self);
end;
procedure performError(AError: String);
begin
Stop;
end;
begin
if not FInitialized then
begin
FInitialized := true;
asm
shaka.polyfill.installAll();
shaka.polyfill.PatchedMediaKeysApple.install();
end;
end;
asm
const videoElement = @self.FVideoHandle;
videoElement.onloadeddata = @doLoadedData;
var currentTime = [];
videoElement.addEventListener("timeupdate", function() {
if (!videoElement.seeking) {
currentTime = videoElement.currentTime;
}
});
let player;
if (shaka.Player.isBrowserSupported())
{
player = new shaka.Player();
player.attach(videoElement);
player.configure('preferredTextLanguage', '');
player.addEventListener('error', function(event) {
var error = event.detail;
@performError(error.code + ' - ' + error.message);
});
videoElement.player = player;
@self.FVideoPlayer = player;
}
end;
end;
procedure TQTXShakaPlayer.OpenMedia(AMediaFile: String; ALicenseServer, ACertServer: String='');
begin
asm
const player = @FVideoPlayer;
const videoElement = @FVideoHandle;
const playerConfig = {
manifest: {
dash: {
ignoreMinBufferTime: true,
},
defaultPresentationDelay: 0
},
streaming: {
lowLatencyMode: true,
inaccurateManifestTolerance: 0,
rebufferingGoal: 0.1,
preferNativeHls: true,
},
abr: {
enabled: true,
useNetworkInformation: false,
defaultBandwidthEstimate: 1000000 / 240,
switchInterval: 120,
},
};
if (@ALicenseServer !== "null" && @ALicenseServer !== null) {
playerConfig.drm = {
advanced: {
"com.apple.fps": {
serverCertificateUri: @ACertServer
}
},
servers: {
"com.widevine.alpha": @ALicenseServer,
"com.apple.fps": @ALicenseServer,
},
};
}
videoElement.playbackRate = 1.0;
player.configure(playerConfig);
player.load(@AMediaFile);
end;
end;
{ TQTXShakaUIPlayer }
procedure TQTXShakaUIPlayer.InitializePlayer;
begin
inherited InitializePlayer;
asm
const video = @FVideoHandle;
video.controls = false;
video.style.width = '100%';
const player = @FVideoPlayer;
player.configure('ui.addPictureInPictureButton', true);
video.parentElement.addEventListener('keyup', function(event) {
if (!video.paused && event.key.toLowerCase() === 'f') {
event.preventDefault();
video.requestFullscreen();
}
});
const ui = new shaka.ui.Overlay(player, video.parentElement, video);
const config = {
'addSeekBar': true,
'seekBarColors': {
base: 'rgba(255,255,255,.2)',
buffered: 'rgba(255,255,255,.4)',
played: '#2f80ed',
}
}
ui.configure(config);
@FShakaUI = ui;
end;
end;
procedure TQTXShakaUIPlayer.SetPoster(const AValue: String);
begin
inherited SetPoster(AValue);
FVideoHandle.Poster := AValue;
end;
{ TQTXVideo }
constructor TQTXVideo.Create(AOwner: TQTXComponent; CB: TQTXVideoConstructor);
begin
inherited Create(AOwner, procedure(Widget: TQTXWidget)
begin
FVideoHandle := document.createElement('video');
Widget.Handle.appendChild(FVideoHandle);
PositionMode := TQTXWidgetPositionMode.cpAbsolute;
if assigned(CB) then CB(self);
end);
end;
(* Example Player
Add this to your index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.12.6/shaka-player.compiled.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.12.6/shaka-player.ui.min.js" ></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.12.6/controls.min.css" />
*)
end.
Posted : 31/01/2025 10:59 am