Module Streaming
Module de streaming securise par tokens pour la MyTelevision API.
Vue d'ensemble
Le module Streaming fournit :
- Acces aux flux par tokens securises
- Liaison IP (IP binding) pour la securite
- Limites d'utilisation par token
- Gestion des cles DRM (AES-128)
- Selection de la qualite video
Architecture
Flux de Streaming
Structure des Fichiers
src/
├── application/
│ ├── dtos/
│ │ └── streaming/
│ │ ├── generate-token.dto.ts
│ │ ├── stream-token-response.dto.ts
│ │ └── validate-token.dto.ts
│ └── services/
│ └── streaming/
│ ├── streaming.service.ts
│ ├── token.service.ts
│ └── drm.service.ts
├── infrastructure/
│ └── guards/
│ └── stream-token.guard.ts
└── presentation/
└── controllers/
└── api/v2/
└── streaming.controller.ts
Modele de Donnees
model StreamingToken {
id String @id @default(uuid())
token String @unique
userId String
profileId String?
contentType MediaType
contentId String
quality VideoQuality
ipAddress String
userAgent String?
maxUses Int @default(5)
usageCount Int @default(0)
expiresAt DateTime
createdAt DateTime @default(now())
lastUsedAt DateTime?
@@index([token])
@@index([userId, contentId])
}
model DrmKey {
id String @id @default(uuid())
contentId String
contentType MediaType
keyId String @unique
key String // Encrypted AES-128 key
iv String // Initialization vector
createdAt DateTime @default(now())
@@index([contentId, contentType])
}
enum VideoQuality {
SD_480
HD_720
FHD_1080
UHD_4K
}
Composants Principaux
StreamingService
@Injectable()
export class StreamingService {
async generateToken(
userId: string,
profileId: string,
dto: GenerateTokenDto,
): Promise<StreamTokenResponseDto>;
async validateToken(token: string, ip: string): Promise<boolean>;
async getStreamUrl(
contentType: MediaType,
contentId: string,
quality: VideoQuality,
): Promise<string>;
async revokeToken(tokenId: string): Promise<void>;
async revokeAllUserTokens(userId: string): Promise<void>;
}
TokenService
@Injectable()
export class TokenService {
generateSecureToken(): string;
async storeToken(data: StreamingTokenData): Promise<StreamingToken>;
async validateAndIncrement(token: string, ip: string): Promise<boolean>;
async isExpired(token: StreamingToken): boolean;
async isExhausted(token: StreamingToken): boolean;
async matchesIp(token: StreamingToken, ip: string): boolean;
}
StreamTokenGuard
@Injectable()
export class StreamTokenGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
const ip = request.ip;
return this.streamingService.validateToken(token, ip);
}
}
DTOs
GenerateTokenDto
export class GenerateTokenDto {
@IsEnum(MediaType)
contentType: MediaType;
@IsUUID()
contentId: string;
@IsOptional()
@IsEnum(VideoQuality)
quality?: VideoQuality = VideoQuality.HD_720;
}
StreamTokenResponseDto
export class StreamTokenResponseDto {
token: string;
streamUrl: string;
hlsUrl: string;
quality: VideoQuality;
expiresAt: Date;
maxUses: number;
}
Securite des Tokens
Proprietes du Token
| Propriete | Valeur | Description |
|---|---|---|
| Longueur | 64 caracteres | Cryptographiquement securise |
| TTL | 4 heures | Configurable par type de contenu |
| Utilisations max | 5 | Empeche le partage de token |
| Liaison IP | Oui | Token lie a l'IP du client |
Regles de Validation
- Le token doit exister et ne pas etre revoque
- Le token ne doit pas etre expire
- Le compteur d'utilisation doit etre inferieur au maximum
- L'IP de la requete doit correspondre a l'IP du token
- L'utilisateur doit avoir un abonnement valide (si contenu premium)
Mesures Anti-Abus
// Rate limiting pour la generation de tokens
@Throttle({ short: { ttl: 1000, limit: 3 } })
@Post('token')
async generateToken() { ... }
// Job de nettoyage des tokens
@Cron('0 */15 * * * *') // Toutes les 15 minutes
async cleanupExpiredTokens() {
await this.streamingService.deleteExpiredTokens();
}
Adaptation de la Qualite
Qualites disponibles selon l'abonnement :
| Abonnement | Qualite Max | Flux Simultanes |
|---|---|---|
| Free | SD_480 | 1 |
| Basic | HD_720 | 2 |
| Premium | FHD_1080 | 4 |
| Ultimate | UHD_4K | 6 |
Integration DRM
Chiffrement AES-128
@Injectable()
export class DrmService {
async generateKey(contentId: string): Promise<DrmKey>;
async getKeyForContent(
contentId: string,
): Promise<{ keyId: string; key: string }>;
async rotateKey(contentId: string): Promise<DrmKey>;
}
Livraison des Cles
GET /streaming/key/:keyId
Authorization: Bearer <stream_token>
Response: Binary AES-128 key
Manifeste HLS
Exemple de manifeste HLS multi-qualite :
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
stream_360p.m3u8?token=xxx
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480
stream_480p.m3u8?token=xxx
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
stream_720p.m3u8?token=xxx
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
stream_1080p.m3u8?token=xxx
Monitoring
Metriques Prometheus
mytelevision_stream_tokens_generated_total{content_type, quality}
mytelevision_stream_tokens_validated_total{status}
mytelevision_stream_tokens_rejected_total{reason}
mytelevision_active_streams_total{content_type}
mytelevision_stream_duration_seconds{content_type}
Alertes
| Alerte | Condition | Action |
|---|---|---|
| HighTokenRejection | >10% rejets en 5 min | Investiguer les abus |
| TokenGenerationSpike | >100 tokens/min | Verifier activite de bots |
| DrmKeyRotationFailed | Erreur rotation cle | Intervention manuelle |
Tests
describe('StreamingService', () => {
it('should generate valid token', async () => {
const result = await streamingService.generateToken(
'user-id',
'profile-id',
{ contentType: MediaType.MOVIE, contentId: 'movie-id' },
);
expect(result.token).toHaveLength(64);
expect(result.streamUrl).toContain('movie-id');
});
it('should reject expired token', async () => {
const token = await createExpiredToken();
const isValid = await streamingService.validateToken(token, '127.0.0.1');
expect(isValid).toBe(false);
});
it('should reject token from different IP', async () => {
const token = await createTokenForIp('192.168.1.1');
const isValid = await streamingService.validateToken(token, '10.0.0.1');
expect(isValid).toBe(false);
});
});