Storage & video with Angular

Audience: Angular developers who let their users upload files, browse them and watch video that is stored and delivered by plycdn. The package gives you a resumable upload area, a file browser, an accessible video player and a client for your backend's plycdn routes. It supports Angular 13 to 22.

The browser never holds your API key and never chooses an organization. Every call goes to your backend, which signs the user in, asks your IPlycdnStorageAuthorizer whether they may do this (the default refuses everything), and adds the key. File bytes go from the browser straight to the bucket's region, authorised only by a short-lived ticket in each part's URL: no cookies, no headers. Your backend setup is in Storage & video with ASP.NET Core; do that first, so the routes the components call exist.

1Install

Copy plycdn-angular-1.4.0.tgz into your application's vendor directory and install it:

npm install ./vendor/plycdn-angular-1.4.0.tgz

That is the whole install. The package installs what it needs automatically from your npm registry or mirror, so make sure yours can serve it. The player loads its streaming code only in browsers that need it; browsers that play the stream natively never download it. Commit the package file and your lockfile, or host the package in your private npm registry so your CI can install it.

2Choose what you import

Import by purpose, from the entry point you need:

Entry point What it holds
@plycdn/angular/storage PlycdnStorageModule, <plycdn-upload>, <plycdn-file-browser>, PlycdnStorageClient, PlycdnUploader
@plycdn/angular/video PlycdnVideoModule, <plycdn-player>, PlycdnPlaybackService, PlycdnPlayerCore
@plycdn/angular What both share: types, failures and messages, the theme, and the framework-free engines (PlycdnUploader, PlycdnPlayerCore) that the two entry points re-export
import { NgModule } from '@angular/core';
import { PlycdnStorageModule } from '@plycdn/angular/storage';
import { PlycdnVideoModule } from '@plycdn/angular/video';

@NgModule({
  imports: [
    PlycdnStorageModule.forRoot({ apiBaseUrl: '/api/plycdn/v1' }),   // uploads and the file browser
    PlycdnVideoModule.forRoot({ apiBaseUrl: '/api/plycdn/v1' }),     // the player: import it only if you play video
  ],
})
export class AppModule {}

apiBaseUrl defaults to /api/plycdn/v1, where the .NET package serves its routes. Set withCredentials: true when your backend is on another origin and uses cookie sign-in. If your site sends a Content-Security-Policy, allow the upload hosts: connect-src https://*.upload.plycdn.com.

3Upload

<plycdn-upload [bucketId]="bucketId" prefix="week-1/" (stored)="browser.refresh()" (failed)="log($event)"></plycdn-upload>
  • Drag files onto the area or choose them. Each file becomes the key prefix + file.name, or whatever [keyFor]="fn" returns.
  • Progress, time left, Pause / Resume, Cancel and Retry. Files beyond [parallelFiles] (default 2) wait their turn; [concurrency] (default 3) is the number of parts of one file in flight.
  • Resumable. A dropped connection is retried, and sending waits while the browser is offline. After a reload or a closed tab, the component lists the unfinished uploads for the bucket; choosing the same file again continues from the last part that arrived. Sessions stay open for 7 days; after that they are no longer offered, and the browser forgets them.
  • Never uploaded twice. Completing an upload can safely be asked again: a lost or timed-out completion is retried, and a file whose upload already completed returns its stored object instead of being sent (and billed) a second time.
  • Privacy. The browser remembers only each unfinished upload's session id, its expiry and, to list it by name, its key. [rememberFileNames]="false" keeps names out of storage (for shared computers): uploads still resume, and the list shows sizes only. On sign-out, call PlycdnUploader.forgetAll().
  • Keyboard. Pause and Resume are one button; after Retry, Cancel or Remove, focus moves to the row's next action (or the next row, or Choose files).
  • Retry appears only where it can help; a refusal such as object_exists or file_type_not_allowed offers Remove instead (which also ends the session).
  • Errors are sentences, not codes: [messages]="{ file_type_not_allowed: 'Only videos and PDFs.' }" rewords (or translates) any of them. PLYCDN_MESSAGES lists the defaults by code, and (failed) carries { code, status, retryable, message }.
  • Every word is in [labels] (defaults: PLYCDN_UPLOAD_LABELS). Other inputs: [accept] (the file picker's filter; empty by default, so every file can be chosen, and the bucket's own allowed types are enforced either way), [multiple], [disabled] and [warnOnLeave] (asks before the page closes mid-upload).

To upload into a video library, give the library's id as bucketId and upload under sources/ (see the Video guide); completing the upload creates the video.

4File browser

<plycdn-file-browser #browser [bucketId]="bucketId" [(prefix)]="folder" (open)="show($event)"></plycdn-file-browser>

Folders come from / in keys, with breadcrumbs and Show more paging ([pageSize], default 100). (open) emits the stored object. A public bucket's object has a url; for a private one, ask your backend for a signed link:

import { Component } from '@angular/core';
import { PlycdnObject } from '@plycdn/angular';
import { PlycdnStorageClient } from '@plycdn/angular/storage';

@Component({ selector: 'app-files', templateUrl: './files.component.html' })
export class FilesComponent {
  bucketId = '...';       // your bucket's id
  folder = '';

  constructor(private storage: PlycdnStorageClient) {}

  show(object: PlycdnObject) {
    this.storage.sign({ bucketId: object.bucketId, objectId: object.id }).subscribe(link => window.open(link.url));
  }
}

To sign on one of the bucket's custom domains, add hostname: sign({ bucketId, objectId, hostname: 'cdn.example.com' }). Your authorizer is asked about the Sign operation before any link is issued.

5Play video

<plycdn-player [libraryId]="libraryId" [videoId]="videoId" (statusChange)="onStatus($event)"></plycdn-player>
  • Links come from your backend. The player asks your backend (POST {apiBaseUrl}/sign with bucketId = the library and videoId) for a playback link; your authorizer decides who may watch (PlycdnOperation.WatchVideo, and GetVideo for the title and status). The video itself streams from *.plycdn.net. Links are renewed at 80 % of their lifetime, and at once if the delivery network refuses one; playback carries on without a reload. [ttlSeconds] sets how long each link lives (your backend's default otherwise).
  • Resumes where this browser last stopped ([resume]="false" turns it off; the position is kept in localStorage under plycdn-position:<videoId>, and nothing breaks where storage is blocked).
  • While a video is still being prepared (playable: the lowest quality is ready) the player checks every 15 seconds and, once every quality is ready, reloads at the same position.
  • Keyboard, while the player has focus: Space or K play/pause, Left/Right 5 seconds, J/L 10 seconds, Up/Down volume, M mute, C next captions track, F full screen, Home/End, 0 to 9 jump to 0 to 90 %, < and > slower and faster. Shortcuts are ignored with Ctrl, Cmd or Alt, and while a text field has focus. Every control is a labelled button reachable with Tab; the seek bar is a slider read in words; changes are announced to screen readers; motion is reduced when the system asks.
  • Quality (Auto first), speed and captions menus; captions are offered when the video has them. Download appears when the library offers downloads. Scrubbing previews show on hover and focus.
  • (statusChange) emits { status: 'loading' | 'ready' | 'error', error, message }. Error codes: video_not_found, video_not_ready, playback_unsupported (a browser that cannot stream), playback_link_failed (three renewals failed; the player offers Try again), and your backend's own refusals such as forbidden. [messages] rewords them like the other components.

If your site sends a Content-Security-Policy, allow the delivery hosts: connect-src https://*.plycdn.net; media-src https://*.plycdn.net blob:; img-src https://*.plycdn.net; worker-src blob:.

Your own player

PlycdnPlaybackService.link(libraryId, videoId, ttlSeconds?) returns the PlaybackLink: url (the stream), nativeUrl (the same stream for native playback, or null), posterUrl, thumbnailsUrl and downloadUrl. PlycdnPlayerCore is the framework-free engine the component uses (link renewal, the token on every request, resume and the keyboard map); give it a <video> element and a getLink function. The Video guide shows a complete example.

6The client and the uploader

PlycdnStorageClient returns Observables: listObjects, getObject, startUpload, getUpload, renewUpload, completeUpload, abortUpload, sign (with a videoId: a PlaybackLink) and getVideo. An organization can hold at most 1,000 open uploads at once; startUpload beyond that fails with plan_limit_reached until one completes, is aborted or expires, so abort uploads you no longer need.

PlycdnUploader is the framework-free uploader the component uses:

import { PlycdnUploader } from '@plycdn/angular/storage';

const uploader = new PlycdnUploader(storage.asSessionApi());
const upload = uploader.upload(file, {
  bucketId, key: 'week-1/intro.mp4',
  onProgress: (sent, total) => {},
  onStateChange: state => {},   // starting, uploading, paused, retrying, offline, completing, stored, failed, aborted
});
upload.pause(); upload.resume();
const object = await upload.done;   // or: await upload.abort();

done rejects with { code, status, retryable } (the server's code, or network_unreachable), or with an AbortError after abort(). retrying and offline are temporary: sending carries on by itself.

Unfinished uploads from earlier visits:

uploader.pendingUploads(bucketId);   // [{ bucketId, key, size, lastModified, uploadId, expiresAt, entry }]
await uploader.forget(pending);      // end that session and stop offering it
uploader.forgetAll(bucketId?);       // forget them all (for example on sign-out); the sessions expire on their own

new PlycdnUploader(api, transport?, store?, { persistNames: false }) keeps object keys out of storage (key is then empty in pendingUploads). The store defaults to localStorage; any object with get, set and delete (and keys for listing) works.

Tags on links: a sign request can carry an optional tags (names and values as in the ASP.NET Core guide). Your backend decides whether they are used: by default tags a browser sends are ignored, so set them on your server.

7Theme

Everything is a CSS custom property on the component or any ancestor: --plycdn-accent, --plycdn-on-accent, --plycdn-text, --plycdn-muted, --plycdn-surface, --plycdn-border, --plycdn-error, --plycdn-success, --plycdn-focus, --plycdn-link, --plycdn-radius, --plycdn-font-family, --plycdn-font-size, --plycdn-spacing, --plycdn-control-height, --plycdn-button-padding, --plycdn-progress-height, --plycdn-progress-track, --plycdn-drop-background, --plycdn-drop-active-background and --plycdn-background; type adds --plycdn-line-height, and the video player takes --plycdn-player-background, --plycdn-player-bar, --plycdn-player-text and --plycdn-player-max-height (default 80vh). Fonts are inherited. data-theme="dark" on the component or an ancestor switches to the dark defaults.

plycdn-upload, plycdn-file-browser, plycdn-player {
  --plycdn-accent: #0b6b57;
  --plycdn-radius: 6px;
}

Sizes are shown in binary units: 1 GB is 2^30 bytes, the same GB your bill uses.

8Troubleshooting

  • Every call fails with forbidden. Your backend has no IPlycdnStorageAuthorizer, or it refused that operation. The default refuses everything.
  • Calls go to the wrong place. apiBaseUrl must reach your backend's plycdn routes. A page that is not JSON (an HTML fallback) is reported as invalid_response, never shown as an empty folder.
  • The player says it cannot play this video. The browser can neither play the stream natively nor load the player's streaming code: check that your Content-Security-Policy allows the delivery hosts and your own scripts.