SpeedOf.Me API - Angular Integration

This example demonstrates how to integrate the SpeedOf.Me speed test API with Angular.

Prerequisites

Quick Start

  1. Create a new Angular project:
    ng new my-speedtest --standalone
    cd my-speedtest
  2. Add the API script to src/index.html:
    <head>
      <!-- other head content -->
      <script src="https://speedof.me/api/api.js"></script>
    </head>
  3. Create type definitions at 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;
  4. Create the service at 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();
        }
    }
  5. Create the component at 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();
        }
    }

Why NgZone?

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.

Configuration

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

Progress Events

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.

Error Codes

onError receives { code, message }.

Code 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.

Using Environment Variables

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;

RxJS Patterns

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);
});

Production Considerations

  1. API Key Security: Use environment files and don't commit keys to version control.
  2. Error Handling: Implement proper error states in your UI.
  3. Loading States: Show skeleton loaders while the API script loads.
  4. NgRx Integration: For larger apps, consider storing test results in NgRx state.

Links