This example demonstrates how to integrate the SpeedOf.Me speed test API with Angular.
ng new my-speedtest --standalone
cd my-speedtest
src/index.html:
<head>
<!-- other head content -->
<script src="https://speedof.me/api/api.js"></script>
</head>
src/types/speedofme.d.ts:
// Every field is always present. A disabled sub-test sends an empty string
// rather than omitting the key, which is what the unions below encode.
interface SpeedTestResult {
download: number;
upload: number | ''; // '' when uploadTestEnabled is false
maxDownload: number;
maxUpload: number | ''; // '' when uploadTestEnabled is false
latency: number | ''; // '' when latencyTestEnabled is false
jitter: number | ''; // '' when latencyTestEnabled is false
testServer: string; // '' when testServerEnabled is false
ip_address: string; // '0.0.0.0' when userInfoEnabled is false
hostname: string; // '' when userInfoEnabled is false
userAgent: string;
testDate: string; // ISO 8601
}
interface SpeedTestProgress {
type: 'download' | 'upload' | 'latency';
pass: number | ''; // '' during the latency phase
percentDone: number; // 0-100
currentSpeed: number | ''; // Mbps, verbose only, always '' on latency events
maxSpeed: number | ''; // Mbps, verbose only, '' until the max exceeds 0
latency: number | ''; // ms, on the final latency event only
jitter: number | ''; // ms, on the final latency event only
}
interface SomApi {
account: string;
domainName: string;
config: {
sustainTime: number;
maxTestPass: number;
testTimeout: number;
testServerEnabled: boolean;
userInfoEnabled: boolean;
latencyTestEnabled: boolean;
uploadTestEnabled: boolean;
progress: { enabled: boolean; verbose: boolean };
};
startTest: () => void;
abortTest: () => void;
onTestCompleted: (result: SpeedTestResult) => void;
onProgress: (progress: SpeedTestProgress) => void;
onError: (error: { code: number; message: string }) => void;
}
declare var SomApi: SomApi;
src/app/services/speed-test.service.ts:
import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
export type TestStatus = 'idle' | 'running' | 'completed' | 'error';
@Injectable({ providedIn: 'root' })
export class SpeedTestService {
private statusSubject = new BehaviorSubject<TestStatus>('idle');
private resultSubject = new BehaviorSubject<SpeedTestResult | null>(null);
private progressSubject = new BehaviorSubject<SpeedTestProgress | null>(null);
private errorSubject = new BehaviorSubject<{ code: number; message: string } | null>(null);
status$ = this.statusSubject.asObservable();
result$ = this.resultSubject.asObservable();
progress$ = this.progressSubject.asObservable();
error$ = this.errorSubject.asObservable();
constructor(private ngZone: NgZone) {
this.initializeApi();
}
private initializeApi(): void {
SomApi.account = 'YOUR_API_KEY';
SomApi.domainName = 'your-domain.com';
SomApi.config.sustainTime = 6;
SomApi.config.progress.verbose = true; // required for progress.currentSpeed
// NgZone.run ensures Angular change detection works
SomApi.onTestCompleted = (result) => {
this.ngZone.run(() => {
this.resultSubject.next(result);
this.progressSubject.next(null);
this.statusSubject.next('completed');
});
};
SomApi.onProgress = (progress) => {
this.ngZone.run(() => {
this.progressSubject.next(progress);
});
};
SomApi.onError = (error) => {
this.ngZone.run(() => {
this.errorSubject.next(error);
this.statusSubject.next('error');
});
};
}
startTest(): void {
this.statusSubject.next('running');
this.resultSubject.next(null);
this.errorSubject.next(null);
SomApi.startTest();
}
}
src/app/components/speed-test/speed-test.component.ts:
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SpeedTestService } from '../../services/speed-test.service';
@Component({
selector: 'app-speed-test',
standalone: true,
imports: [CommonModule],
template: `
<div class="speed-test">
<button
(click)="startTest()"
[disabled]="(status$ | async) === 'running'"
>
{{ (status$ | async) === 'running' ? 'Testing...' : 'Start Test' }}
</button>
<div *ngIf="progress$ | async as progress" class="progress">
<p>{{ progress.type | titlecase }} Test</p>
<div class="progress-bar">
<div [style.width.%]="progress.percentDone"></div>
</div>
<p *ngIf="progress.currentSpeed !== ''">{{ progress.currentSpeed | number:'1.2-2' }} Mbps</p>
</div>
<div *ngIf="result$ | async as result" class="results">
<p>Download: {{ result.download }} Mbps</p>
<p>Upload: {{ result.upload }} Mbps</p>
<p>Latency: {{ result.latency }} ms</p>
<p>Jitter: {{ result.jitter }} ms</p>
</div>
<div *ngIf="error$ | async as error" class="error">
Error {{ error.code }}: {{ error.message }}
</div>
</div>
`
})
export class SpeedTestComponent {
// inject() in the field initializer, not a constructor parameter: with
// native class fields the initializers below run before a constructor
// parameter property would be assigned.
private speedTestService = inject(SpeedTestService);
status$ = this.speedTestService.status$;
result$ = this.speedTestService.result$;
progress$ = this.speedTestService.progress$;
error$ = this.speedTestService.error$;
startTest(): void {
this.speedTestService.startTest();
}
}
The SpeedOf.Me API callbacks execute outside Angular's zone, which means change detection won't trigger automatically. Wrapping callback code in ngZone.run() ensures the UI updates properly.
SomApi.account = 'YOUR_API_KEY';
SomApi.domainName = 'your-domain.com';
SomApi.config.sustainTime = 6; // 1-8 seconds
SomApi.config.testServerEnabled = true;
SomApi.config.userInfoEnabled = true;
SomApi.config.latencyTestEnabled = true;
SomApi.config.uploadTestEnabled = true;
SomApi.config.progress.enabled = true;
SomApi.config.progress.verbose = true; // fills in currentSpeed and maxSpeed
onProgress fires for three phases: latency, then download, then upload.
progress.type // 'download', 'upload' or 'latency'
progress.pass // pass number, '' during the latency phase
progress.percentDone // 0-100
progress.currentSpeed // Mbps, only with progress.verbose, '' otherwise
progress.maxSpeed // Mbps, only with progress.verbose, '' otherwise
progress.latency // ms, on the final latency event only, '' otherwise
progress.jitter // ms, on the final latency event only, '' otherwise
Without progress.verbose = true the speed fields arrive as empty strings, and they stay empty on latency events even when verbose is on, so guard before formatting them.
onError receives { code, message }.
1001 Invalid Account: the API key is not recognised or is inactive1002 Domain Mismatch: the page's own domain does not match your registration2001 Test Error2002 Invalid server response2003 Request timeout2004 Test timeout: the whole test exceeded config.testTimeout2005 Speed test could not start (status N)2006 Speed test engine did not loadCode 2006, and 2005 with status 0, almost always mean the request never left the browser, usually because of an ad blocker, a content blocker or a privacy extension. The one thing worth checking on your side is a frame-src Content-Security-Policy: it has to allow https://speedof.me, or the engine iframe can never load and every test fails with 2006. A 2005 carrying any other status is a real HTTP response, so treat it as a server-side failure.
Store your API key in environment.ts:
// src/environments/environment.ts
export const environment = {
production: false,
speedOfMeApiKey: 'YOUR_API_KEY',
speedOfMeDomain: 'your-domain.com'
};
Then use in your service:
import { environment } from '../../environments/environment';
SomApi.account = environment.speedOfMeApiKey;
SomApi.domainName = environment.speedOfMeDomain;
The service exposes observables that you can compose with other RxJS operators:
// Combine with other observables
combineLatest([this.status$, this.result$]).pipe(
filter(([status, result]) => status === 'completed' && result !== null)
).subscribe(([_, result]) => {
console.log('Test completed:', result);
});