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:
interface SpeedTestResult {
download: number;
upload: number;
latency: number;
jitter: number;
testServer?: string;
ip_address?: string;
hostname?: string;
}
interface SpeedTestProgress {
testType: 'download' | 'upload';
currentSpeed: number;
percentComplete: number;
passNumber: number;
}
interface SomApi {
account: string;
domainName: string;
config: {
sustainTime: number;
testServerEnabled: boolean;
userInfoEnabled: boolean;
latencyTestEnabled: boolean;
uploadTestEnabled: boolean;
};
startTest: () => 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 = 4;
// 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.component.ts:
import { Component } 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="result$ | async as result">
<p>Download: {{ result.download }} Mbps</p>
<p>Upload: {{ result.upload }} Mbps</p>
<p>Latency: {{ result.latency }} ms</p>
</div>
</div>
`
})
export class SpeedTestComponent {
status$ = this.speedTestService.status$;
result$ = this.speedTestService.result$;
constructor(private speedTestService: SpeedTestService) {}
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 = 4; // 1-8 seconds
SomApi.config.testServerEnabled = true;
SomApi.config.userInfoEnabled = true;
SomApi.config.latencyTestEnabled = true;
SomApi.config.uploadTestEnabled = true;
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);
});