// Package playback — стриминг, транскодинг, события прослушивания, история, реакции и // состояние плеера (docs/playback.md). package playback // Качество стрима. const ( QualityAuto = "auto" QualityOriginal = "original" QualityHigh = "high" QualityMedium = "medium" QualityLow = "low" ) // Целевой битрейт Opus, кбит/с. var opusKbps = map[string]int{QualityHigh: 256, QualityMedium: 128, QualityLow: 64} // Форматы, которые браузеры не играют: их всегда отдаём транскодом. var browserUnplayable = map[string]bool{"ape": true, "wv": true, "aiff": true, "aif": true} var losslessCodecs = map[string]bool{"flac": true, "alac": true, "wavpack": true, "ape": true, "tta": true} func lossless(codec string) bool { return losslessCodecs[codec] || len(codec) > 4 && codec[:4] == "pcm_" } // Target решает, что отдать: "" — оригинал, иначе качество транскода. // Перекодировать вверх бессмысленно: lossy-исходник не выше целевого битрейта отдаётся как есть. func Target(q, format, codec string, bitrate int) string { switch q { case QualityHigh, QualityMedium, QualityLow: default: // auto, original, пусто if browserUnplayable[format] { return QualityHigh } return "" } if lossless(codec) || browserUnplayable[format] { return q } if bitrate > 0 && bitrate <= opusKbps[q]*1000*11/10 { return "" } return q } // Пороги засчитывания (docs/playback.md#события-прослушивания). const ( countCapMs = 4 * 60 * 1000 // засчитано после min(50% длительности, 4 мин) historyMinMs = 30 * 1000 // в историю — от 30 с earlySkipMs = 30 * 1000 // ранний скип — до 30 с ) // Outcome — что значит событие окончания попытки (play_end / skip) для статистики. type Outcome struct { Counted bool // засчитанное прослушивание Complete bool // доиграно до конца Skip bool EarlySkip bool History bool // попадает в историю PlayedMs int } // Judge оценивает событие окончания попытки. durationMs — длительность трека. func Judge(eventType string, playedMs, durationMs int) Outcome { o := Outcome{PlayedMs: max(playedMs, 0)} switch eventType { case "play_end": o.Complete, o.Counted = true, true case "skip": o.Skip = true o.EarlySkip = o.PlayedMs < earlySkipMs threshold := min(durationMs/2, countCapMs) o.Counted = durationMs > 0 && o.PlayedMs >= threshold default: return Outcome{} } o.History = o.PlayedMs >= historyMinMs || (durationMs > 0 && durationMs < historyMinMs && o.Complete) return o }