diff --git a/playkit/src/main/java/com/kaltura/playkit/PKLog.java b/playkit/src/main/java/com/kaltura/playkit/PKLog.java index 2442dc55d..8d1a6ccd1 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PKLog.java +++ b/playkit/src/main/java/com/kaltura/playkit/PKLog.java @@ -63,7 +63,7 @@ public class PKLog { @NonNull public final String tag; - private int level = VERBOSE; + private int level; public enum Level { verbose(VERBOSE), debug(DEBUG), info(INFO), warn(WARN), error(ERROR), off(Integer.MAX_VALUE); @@ -103,6 +103,9 @@ private PKLog(@NonNull String tag) { return new PKLog(tag); } + public boolean isLoggable(Level level) { + return Log.isLoggable(tag, level.value); + } // VERBOSE diff --git a/playkit/src/main/java/com/kaltura/playkit/PlayKitManager.java b/playkit/src/main/java/com/kaltura/playkit/PlayKitManager.java index 75dc01850..e2b920128 100644 --- a/playkit/src/main/java/com/kaltura/playkit/PlayKitManager.java +++ b/playkit/src/main/java/com/kaltura/playkit/PlayKitManager.java @@ -15,7 +15,9 @@ import android.content.Context; import androidx.annotation.Nullable; +import com.google.gson.Gson; import com.kaltura.playkit.player.MediaSupport; +import com.kaltura.playkit.profiler.PlayKitProfiler; import java.util.HashMap; import java.util.Map; @@ -54,7 +56,6 @@ static PKPlugin createPlugin(String name) { } public static Player loadPlayer(Context context, @Nullable PKPluginConfigs pluginConfigs, MessageBus messageBus) { - MediaSupport.initializeDrm(context, null); if (shouldSendDeviceCapabilitiesReport) { @@ -67,13 +68,18 @@ public static Player loadPlayer(Context context, @Nullable PKPluginConfigs plugi } public static Player loadPlayer(Context context, @Nullable PKPluginConfigs pluginConfigs) { - return loadPlayer(context, pluginConfigs, null); } - public static void disableDeviceCapabilitiesReport() { shouldSendDeviceCapabilitiesReport = false; } + + public static final class ProfilerConfig { + public String postURL; + public float sendPercentage; + + private ProfilerConfig() {} + } } diff --git a/playkit/src/main/java/com/kaltura/playkit/Utils.java b/playkit/src/main/java/com/kaltura/playkit/Utils.java index a08e53e05..b33815254 100644 --- a/playkit/src/main/java/com/kaltura/playkit/Utils.java +++ b/playkit/src/main/java/com/kaltura/playkit/Utils.java @@ -20,11 +20,14 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; -import androidx.annotation.NonNull; - import android.telephony.TelephonyManager; import android.util.Base64; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import java.io.BufferedInputStream; @@ -45,6 +48,7 @@ import static android.content.Context.UI_MODE_SERVICE; import static com.kaltura.playkit.utils.Consts.HTTP_METHOD_GET; import static com.kaltura.playkit.utils.Consts.HTTP_METHOD_POST; +import static com.kaltura.playkit.utils.Consts.MILLISECONDS_MULTIPLIER_FLOAT; /** * @hide @@ -293,4 +297,77 @@ public static String getDeviceType(Context context) { } return deviceType; } + + public static class GsonObject { + private final JsonObject jo = new JsonObject(); + + public JsonObject jsonObject() { + return jo; + } + + public GsonObject add(String key, String value) { + jo.addProperty(key, value); + return this; + } + + public GsonObject add(String key, Number value) { + jo.addProperty(key, value); + return this; + } + + public GsonObject add(String key, long value) { + jo.addProperty(key, value); + return this; + } + + public GsonObject add(String key, float value) { + jo.addProperty(key, value); + return this; + } + + public GsonObject add(String key, boolean value) { + jo.addProperty(key, value); + return this; + } + + public GsonObject add(String key, Throwable value) { + jo.addProperty(key, "" + value); + return this; + } + + public GsonObject add(String key, JsonElement value) { + jo.add(key, value); + return this; + } + + public GsonObject add(String key, @Nullable GsonObject value) { + jo.add(key, value != null ? value.jo : null); + return this; + } + + public GsonObject addAll(@Nullable JsonObject otherJo) { + if (otherJo != null) { + for (Map.Entry entry : otherJo.entrySet()) { + jo.add(entry.getKey(), entry.getValue()); + } + } + return this; + } + + public GsonObject addAll(@Nullable GsonObject otherGo) { + if (otherGo != null) { + for (Map.Entry entry : otherGo.jo.entrySet()) { + jo.add(entry.getKey(), entry.getValue()); + } + } + return this; + } + + public GsonObject addTime(String key, long millis) { + jo.addProperty(key, millis / MILLISECONDS_MULTIPLIER_FLOAT); + return this; + } + + public void end() {} + } } diff --git a/playkit/src/main/java/com/kaltura/playkit/player/Profiler.java b/playkit/src/main/java/com/kaltura/playkit/player/Profiler.java index ab8510d74..984bbd2d7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/player/Profiler.java +++ b/playkit/src/main/java/com/kaltura/playkit/player/Profiler.java @@ -4,6 +4,8 @@ import com.kaltura.android.exoplayer2.analytics.AnalyticsListener; import com.kaltura.playkit.PKMediaConfig; +import com.kaltura.playkit.Utils; +import com.kaltura.playkit.profiler.PlayKitProfiler; import okhttp3.EventListener; @@ -28,4 +30,20 @@ public void onDurationChanged(long duration) {/*NOOP*/} public void onApplicationPaused() {/*NOOP*/} public void onApplicationResumed() {/*NOOP*/} + + public static class Event extends Utils.GsonObject { + + private final PlayKitProfiler profiler; + + public Event(PlayKitProfiler profiler, String name) { + this.profiler = profiler; + add("_ts", profiler.timestamp()); + add("_name", name); + } + + @Override + public void end() { + profiler.append(jsonObject()); + } + } } diff --git a/playkit/src/main/java/com/kaltura/playkit/profiler/ExoPlayerProfilingListener.java b/playkit/src/main/java/com/kaltura/playkit/profiler/ExoPlayerProfilingListener.java index 5eac9dd71..826bab119 100644 --- a/playkit/src/main/java/com/kaltura/playkit/profiler/ExoPlayerProfilingListener.java +++ b/playkit/src/main/java/com/kaltura/playkit/profiler/ExoPlayerProfilingListener.java @@ -1,6 +1,5 @@ package com.kaltura.playkit.profiler; - import android.util.Pair; import androidx.annotation.NonNull; @@ -8,10 +7,12 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.kaltura.android.exoplayer2.C; import com.kaltura.android.exoplayer2.Format; import com.kaltura.android.exoplayer2.PlaybackException; import com.kaltura.android.exoplayer2.PlaybackParameters; import com.kaltura.android.exoplayer2.Player; +import com.kaltura.android.exoplayer2.TracksInfo; import com.kaltura.android.exoplayer2.analytics.AnalyticsListener; import com.kaltura.android.exoplayer2.decoder.DecoderCounters; import com.kaltura.android.exoplayer2.decoder.DecoderReuseEvaluation; @@ -19,17 +20,13 @@ import com.kaltura.android.exoplayer2.metadata.Metadata; import com.kaltura.android.exoplayer2.source.LoadEventInfo; import com.kaltura.android.exoplayer2.source.MediaLoadData; -import com.kaltura.android.exoplayer2.source.TrackGroup; -import com.kaltura.android.exoplayer2.source.TrackGroupArray; -import com.kaltura.android.exoplayer2.trackselection.ExoTrackSelection; -import com.kaltura.android.exoplayer2.trackselection.TrackSelection; -import com.kaltura.android.exoplayer2.trackselection.TrackSelectionArray; import com.kaltura.android.exoplayer2.video.VideoSize; import com.kaltura.playkit.PKPlaybackException; +import com.kaltura.playkit.Utils; import com.kaltura.playkit.player.PKPlayerErrorType; +import com.kaltura.playkit.player.Profiler.Event; import java.io.IOException; -import java.util.LinkedHashSet; import static com.kaltura.android.exoplayer2.C.DATA_TYPE_AD; import static com.kaltura.android.exoplayer2.C.DATA_TYPE_DRM; @@ -54,10 +51,6 @@ import static com.kaltura.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; import static com.kaltura.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.kaltura.playkit.profiler.PlayKitProfiler.MSEC_MULTIPLIER_FLOAT; -import static com.kaltura.playkit.profiler.PlayKitProfiler.field; -import static com.kaltura.playkit.profiler.PlayKitProfiler.joinFields; -import static com.kaltura.playkit.profiler.PlayKitProfiler.nullable; -import static com.kaltura.playkit.profiler.PlayKitProfiler.timeField; class ExoPlayerProfilingListener implements AnalyticsListener { @@ -70,8 +63,8 @@ class ExoPlayerProfilingListener implements AnalyticsListener { this.profiler = profiler; } - public void log(String event, String... strings) { - profiler.logWithPlaybackInfo(event, strings); + private Event log(String event) { + return profiler.logWithPlaybackInfo(event); } private String trackSelectionReasonString(int trackSelectionReason) { @@ -85,13 +78,17 @@ private String trackSelectionReasonString(int trackSelectionReason) { } } - private String trackFormatString(@Nullable Format trackFormat) { + private JsonObject trackFormatMap(@Nullable Format trackFormat) { if (trackFormat == null) { return null; } - return joinFields(nullable("id", trackFormat.id), "bitrate=" + trackFormat.bitrate, - nullable("codecs", trackFormat.codecs), nullable("language", trackFormat.language)); + return new Utils.GsonObject() + .add("id", trackFormat.id) + .add("bitrate", trackFormat.bitrate) + .add("codecs", trackFormat.codecs) + .add("language", trackFormat.language) + .jsonObject(); } private String trackTypeString(int trackType) { @@ -118,13 +115,12 @@ private String dataTypeString(int dataType) { } @Override - public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) { - log("IsPlayingChanged", - field("isPlaying", isPlaying)); + public void onIsPlayingChanged(@NonNull EventTime eventTime, boolean isPlaying) { + profiler.logWithPlaybackInfo("IsPlayingChanged").add("isPlaying", isPlaying).end(); } @Override - public void onPlaybackStateChanged(EventTime eventTime, int playbackState) { + public void onPlaybackStateChanged(@NonNull EventTime eventTime, int playbackState) { String state; switch (playbackState) { case Player.STATE_IDLE: @@ -142,29 +138,31 @@ public void onPlaybackStateChanged(EventTime eventTime, int playbackState) { default: return; } - log("PlayerStateChanged", field("state", state), field("shouldPlay", shouldPlay)); + log("PlayerStateChanged").add("state", state).add("shouldPlay", shouldPlay).end(); } @Override - public void onPlayWhenReadyChanged(EventTime eventTime, boolean playWhenReady, int reason) { + public void onPlayWhenReadyChanged(@NonNull EventTime eventTime, boolean playWhenReady, int reason) { shouldPlay = playWhenReady; } @Override - public void onTimelineChanged(EventTime eventTime, int reason) { + public void onTimelineChanged(@NonNull EventTime eventTime, int reason) { } @Override - public void onPositionDiscontinuity(EventTime eventTime, Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, int reason) { + public void onPositionDiscontinuity(@NonNull EventTime eventTime, + @NonNull Player.PositionInfo oldPosition, + @NonNull Player.PositionInfo newPosition, + int reason) { String reasonString; switch (reason) { case DISCONTINUITY_REASON_AUTO_TRANSITION: reasonString = "AutoTransition"; break; case DISCONTINUITY_REASON_SEEK: - log("SeekProcessed"); + log("SeekProcessed").end(); reasonString = "Seek"; break; case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: @@ -183,18 +181,21 @@ public void onPositionDiscontinuity(EventTime eventTime, Player.PositionInfo old reasonString = "Unknown:" + reason; } - log("PositionDiscontinuity", field("reason", reasonString)); + log("PositionDiscontinuity") + .add("reason", reasonString) + .end(); } @Override - public void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters) { - log("PlaybackParametersChanged", - field("speed", playbackParameters.speed), - field("pitch", playbackParameters.pitch)); + public void onPlaybackParametersChanged(@NonNull EventTime eventTime, PlaybackParameters playbackParameters) { + log("PlaybackParametersChanged") + .add("speed", playbackParameters.speed) + .add("pitch", playbackParameters.pitch) + .end(); } @Override - public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { + public void onRepeatModeChanged(@NonNull EventTime eventTime, int repeatMode) { String strMode; switch (repeatMode) { case Player.REPEAT_MODE_OFF: @@ -210,76 +211,84 @@ public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { strMode = "Unknown(" + repeatMode + ")"; break; } - log("RepeatModeChanged", field("repeatMode", strMode)); + log("RepeatModeChanged") + .add("repeatMode", strMode) + .end(); } @Override - public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { - log("ShuffleModeChanged", field("shuffleModeEnabled", shuffleModeEnabled)); + public void onShuffleModeChanged(@NonNull EventTime eventTime, boolean shuffleModeEnabled) { + log("ShuffleModeChanged") + .add("shuffleModeEnabled", shuffleModeEnabled) + .end(); } @Override - public void onAudioCodecError(EventTime eventTime, Exception audioCodecError) { - log("PlayerError", field("type", "audioCodecError"), "cause={" + audioCodecError.getCause() + "}"); + public void onIsLoadingChanged(@NonNull EventTime eventTime, boolean isLoading) { + log("LoadingChanged") + .add("isLoading", isLoading) + .end(); } @Override - public void onAudioSinkError(EventTime eventTime, Exception audioSinkError) { - log("PlayerError", field("type", "audioSinkError"), "cause={" + audioSinkError.getCause() + "}"); + public void onAudioCodecError(@NonNull EventTime eventTime, Exception audioCodecError) { + log("PlayerError") + .add("type", "audioCodecError") + .add("cause", audioCodecError.getCause()) + .end(); } @Override - public void onVideoCodecError(EventTime eventTime, Exception videoCodecError) { - log("PlayerError", field("type", "videoCodecError"), "cause={" + videoCodecError.getCause() + "}"); + public void onAudioSinkError(@NonNull EventTime eventTime, Exception audioSinkError) { + log("PlayerError") + .add("type", "audioSinkError") + .add("cause", audioSinkError.getCause()) + .end(); } @Override - public void onPlayerError(EventTime eventTime, PlaybackException playbackException) { + public void onVideoCodecError(@NonNull EventTime eventTime, Exception videoCodecError) { + log("PlayerError") + .add("type", "videoCodecError") + .add("cause", videoCodecError.getCause()) + .end(); + } + + @Override + public void onPlayerError(@NonNull EventTime eventTime, @NonNull PlaybackException playbackException) { Pair exceptionPair = PKPlaybackException.getPlaybackExceptionType(playbackException); String type = exceptionPair.first.toString(); - log("PlayerError", field("type", type), "cause={" + playbackException.getCause() + "}"); + log("PlayerError") + .add("type", type) + .add("cause", playbackException.getCause()) + .end(); } @Override - public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - LinkedHashSet trackGroupSet = new LinkedHashSet<>(trackGroups.length); - for (int i = 0; i < trackSelections.length; i++) { - final TrackSelection trackSelection = trackSelections.get(i); - if (trackSelection != null) { - trackGroupSet.add(trackSelection.getTrackGroup()); - } - } - - // Add the rest - for (int i = 0; i < trackGroups.length; i++) { - final TrackGroup trackGroup = trackGroups.get(i); - trackGroupSet.add(trackGroup); + public void onTracksInfoChanged(@NonNull EventTime eventTime, @NonNull TracksInfo tracksInfo) { + if (tracksInfo.getTrackGroupInfos().size() <= 0) { + return; } - JsonArray jTrackGroups = new JsonArray(trackGroups.length); - for (TrackGroup trackGroup : trackGroupSet) { - JsonArray jTrackGroup = new JsonArray(trackGroup.length); - for (int j = 0; j < trackGroup.length; j++) { - final Format format = trackGroup.getFormat(j); - jTrackGroup.add(toJSON(format)); + // All the formats will be added to the TrackGroup's JSON + JsonArray jTrackGroups = new JsonArray(tracksInfo.getTrackGroupInfos().size()); + JsonArray jTextTrackSelections = new JsonArray(); + + for (TracksInfo.TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) { + JsonArray jTrackGroup = new JsonArray(trackGroupInfo.getTrackGroup().length); + for (int trackGroupIndex = 0; trackGroupIndex < trackGroupInfo.getTrackGroup().length; trackGroupIndex++) { + jTrackGroup.add(toJSON(trackGroupInfo.getTrackGroup().getFormat(trackGroupIndex))); + if (trackGroupInfo.getTrackType() == C.TRACK_TYPE_TEXT && trackGroupInfo.isSelected()) { + jTextTrackSelections.add(toJSON(trackGroupInfo.getTrackGroup().getFormat(trackGroupIndex))); + } } jTrackGroups.add(jTrackGroup); } - - JsonArray jTrackSelections = new JsonArray(trackSelections.length); - for (int i = 0; i < trackSelections.length; i++) { - ExoTrackSelection trackSelection = null; - if (trackSelections.get(i) instanceof ExoTrackSelection) { - trackSelection = (ExoTrackSelection) trackSelections.get(i); - } - final Format selectedFormat = trackSelection == null ? null : trackSelection.getSelectedFormat(); - jTrackSelections.add(toJSON(selectedFormat)); - } - - log("TracksChanged", - field("available", jTrackGroups.toString()), - field("selected", jTrackSelections.toString())); + log("TracksChanged") + .add("available", jTrackGroups) + .add("selected", jTextTrackSelections) + .end(); } private JsonObject toJSON(@Nullable Format format) { @@ -301,159 +310,210 @@ private JsonObject toJSON(@Nullable Format format) { return jsonObject; } - private void logLoadingEvent(String event, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, Boolean wasCanceled) { - String dataTypeString = dataTypeString(mediaLoadData.dataType); + private void logLoadingEvent(String event, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, @Nullable IOException error, @Nullable Boolean wasCanceled) { String dataTypeString = dataTypeString(mediaLoadData.dataType); String trackTypeString = trackTypeString(mediaLoadData.trackType); if (dataTypeString == null) { return; } - log(event, - timeField("time", loadEventInfo.elapsedRealtimeMs - profiler.sessionStartTime), field("uri", loadEventInfo.dataSpec.uri.toString()), - field("dataType", dataTypeString), field("trackType", trackTypeString), - trackFormatString(mediaLoadData.trackFormat), field("reason", trackSelectionReasonString(mediaLoadData.trackSelectionReason)), - timeField("rangeStart", mediaLoadData.mediaStartTimeMs), timeField("rangeEnd", mediaLoadData.mediaEndTimeMs), - timeField("loadTime", loadEventInfo.loadDurationMs), field("bytes", loadEventInfo.bytesLoaded), - field("error", error != null ? "{" + error.getMessage() + "}" : null), wasCanceled == null ? null : field("canceled", wasCanceled)); + final Utils.GsonObject e = log(event) + .addTime("time", loadEventInfo.elapsedRealtimeMs - profiler.sessionStartTime) + .add("uri", loadEventInfo.dataSpec.uri.toString()) + .add("dataType", dataTypeString) + .add("trackType", trackTypeString) + .addAll(trackFormatMap(mediaLoadData.trackFormat)) + .add("reason", trackSelectionReasonString(mediaLoadData.trackSelectionReason)) + .addTime("rangeStart", mediaLoadData.mediaStartTimeMs) + .addTime("rangeEnd", mediaLoadData.mediaEndTimeMs) + .addTime("loadTime", loadEventInfo.loadDurationMs) + .add("bytes", loadEventInfo.bytesLoaded) + .add("error", error != null ? error.getMessage() : null); + + if (wasCanceled != null) { + e.add("canceled", wasCanceled); + } + e.end(); } @Override - public void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - logLoadingEvent("LoadStarted", loadEventInfo, mediaLoadData, null, null); + public void onLoadStarted(@NonNull EventTime eventTime, LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData) { profiler.maybeLogServerInfo(loadEventInfo.uri); } @Override - public void onLoadCompleted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + public void onLoadCompleted(@NonNull EventTime eventTime, @NonNull LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData) { logLoadingEvent("LoadCompleted", loadEventInfo, mediaLoadData, null, null); } @Override - public void onLoadCanceled(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + public void onLoadCanceled(@NonNull EventTime eventTime, @NonNull LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData) { logLoadingEvent("LoadCanceled", loadEventInfo, mediaLoadData, null, null); } @Override - public void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { + public void onLoadError(@NonNull EventTime eventTime, @NonNull LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData, @NonNull IOException error, boolean wasCanceled) { logLoadingEvent("LoadError", loadEventInfo, mediaLoadData, error, wasCanceled); } @Override - public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + public void onDownstreamFormatChanged(@NonNull EventTime eventTime, MediaLoadData mediaLoadData) { String trackTypeString = trackTypeString(mediaLoadData.trackType); if (trackTypeString == null) { return; } - log("DownstreamFormatChanged", field("trackType", trackTypeString), trackFormatString(mediaLoadData.trackFormat), field("reason", trackSelectionReasonString(mediaLoadData.trackSelectionReason))); + log("DownstreamFormatChanged") + .add("trackType", trackTypeString) + .addAll(trackFormatMap(mediaLoadData.trackFormat)) + .add("reason", trackSelectionReasonString(mediaLoadData.trackSelectionReason)) + .end(); } @Override - public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + public void onUpstreamDiscarded(@NonNull EventTime eventTime, MediaLoadData mediaLoadData) { String trackTypeString = trackTypeString(mediaLoadData.trackType); if (trackTypeString == null) { return; } - log("UpstreamDiscarded", field("trackType", trackTypeString), field("start", mediaLoadData.mediaStartTimeMs / MSEC_MULTIPLIER_FLOAT), field("end", mediaLoadData.mediaEndTimeMs / MSEC_MULTIPLIER_FLOAT)); + log("UpstreamDiscarded") + .add("trackType", trackTypeString) + .add("start", mediaLoadData.mediaStartTimeMs / MSEC_MULTIPLIER_FLOAT) + .add("end", mediaLoadData.mediaEndTimeMs / MSEC_MULTIPLIER_FLOAT) + .end(); } @Override - public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { - log("BandwidthSample", - field("bandwidth", bitrateEstimate), - timeField("totalLoadTime", totalLoadTimeMs), - field("totalBytesLoaded", totalBytesLoaded) - ); + public void onBandwidthEstimate(@NonNull EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + log("BandwidthSample") + .add("bandwidth", bitrateEstimate) + .addTime("totalLoadTime", totalLoadTimeMs) + .add("totalBytesLoaded", totalBytesLoaded) + .end(); } @Override - public void onMetadata(EventTime eventTime, Metadata metadata) { + public void onMetadata(@NonNull EventTime eventTime, @NonNull Metadata metadata) { } @Override - public void onAudioEnabled(EventTime eventTime, DecoderCounters counters) { + public void onAudioEnabled(@NonNull EventTime eventTime, @NonNull DecoderCounters counters) { } @Override - public void onVideoEnabled(EventTime eventTime, DecoderCounters counters) { + public void onVideoEnabled(@NonNull EventTime eventTime, @NonNull DecoderCounters counters) { } @Override - public void onVideoDecoderInitialized(EventTime eventTime, String decoderName, long initializedTimestampMs, long initializationDurationMs) { - log("DecoderInitialized", field("name", decoderName), field("duration", initializationDurationMs / MSEC_MULTIPLIER_FLOAT)); + public void onVideoDecoderInitialized(@NonNull EventTime eventTime, @NonNull String decoderName, long initializedTimestampMs, long initializationDurationMs) { + log("DecoderInitialized") + .add("name", decoderName) + .add("duration", initializationDurationMs / MSEC_MULTIPLIER_FLOAT) + .end(); + } + + @Override + public void onVideoInputFormatChanged(@NonNull EventTime eventTime, @NonNull Format format, DecoderReuseEvaluation decoderReuseEvaluation) { + log("DecoderInputFormatChanged") + .add("id", format.id) + .add("codecs", format.codecs) + .add("bitrate", format.bitrate) + .end(); + + JsonArray jVideoTrackSelections = new JsonArray(1); + jVideoTrackSelections.add(toJSON(format)); + log("TracksChanged") + .add("selected", jVideoTrackSelections) + .end(); } @Override - public void onVideoInputFormatChanged(EventTime eventTime, Format format, DecoderReuseEvaluation decoderReuseEvaluation) { - log("DecoderInputFormatChanged", field("id", format.id), field("codecs", format.codecs), field("bitrate", format.bitrate)); + public void onAudioInputFormatChanged(@NonNull EventTime eventTime, @NonNull Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { + JsonArray jAudioTrackSelections = new JsonArray(1); + jAudioTrackSelections.add(toJSON(format)); + log("TracksChanged") + .add("selected", jAudioTrackSelections) + .end(); } @Override - public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) { + public void onVideoDisabled(@NonNull EventTime eventTime, @NonNull DecoderCounters decoderCounters) { } @Override - public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + public void onAudioUnderrun(@NonNull EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { } @Override - public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { - log("DroppedFrames", field("count", droppedFrames), field("time", elapsedMs / MSEC_MULTIPLIER_FLOAT)); + public void onDroppedVideoFrames(@NonNull EventTime eventTime, int droppedFrames, long elapsedMs) { + log("DroppedFrames") + .add("count", droppedFrames) + .add("time", elapsedMs / MSEC_MULTIPLIER_FLOAT) + .end(); } @Override - public void onVideoSizeChanged(EventTime eventTime, @NonNull VideoSize videoSize) { - log("VideoSizeChanged", field("width", videoSize.width), field("height", videoSize.height)); + public void onVideoSizeChanged(@NonNull EventTime eventTime, @NonNull VideoSize videoSize) { + log("VideoSizeChanged") + .add("width", videoSize.width) + .add("height", videoSize.height) + .end(); } @Override - public void onRenderedFirstFrame(EventTime eventTime, Object output, long renderTimeMs) { - log("RenderedFirstFrame"); + public void onRenderedFirstFrame(@NonNull EventTime eventTime, @NonNull Object output, long renderTimeMs) { + log("RenderedFirstFrame") + .add("renderTimeMs", renderTimeMs) + .end(); } @Override - public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { - log("ViewportSizeChange", field("width", width), field("height", height)); + public void onSurfaceSizeChanged(@NonNull EventTime eventTime, int width, int height) { + log("ViewportSizeChange") + .add("width", width) + .add("height", height) + .end(); } @Override - public void onVolumeChanged(EventTime eventTime, float volume) { - log("VolumeChanged", field("volume", volume)); + public void onVolumeChanged(@NonNull EventTime eventTime, float volume) { + log("VolumeChanged") + .add("volume", volume) + .end(); } @Override - public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) { - log("DrmSessionAcquired"); + public void onDrmSessionAcquired(@NonNull EventTime eventTime, @DrmSession.State int state) { + log("DrmSessionAcquired").end(); } @Override - public void onDrmSessionReleased(EventTime eventTime) { - log("DrmSessionReleased"); + public void onDrmSessionReleased(@NonNull EventTime eventTime) { + log("DrmSessionReleased").end(); } @Override - public void onDrmKeysLoaded(EventTime eventTime) { + public void onDrmKeysLoaded(@NonNull EventTime eventTime) { } @Override - public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + public void onDrmSessionManagerError(@NonNull EventTime eventTime, @NonNull Exception error) { } @Override - public void onDrmKeysRestored(EventTime eventTime) { + public void onDrmKeysRestored(@NonNull EventTime eventTime) { } @Override - public void onDrmKeysRemoved(EventTime eventTime) { + public void onDrmKeysRemoved(@NonNull EventTime eventTime) { } } diff --git a/playkit/src/main/java/com/kaltura/playkit/profiler/OkHttpListener.java b/playkit/src/main/java/com/kaltura/playkit/profiler/OkHttpListener.java index cc617b02b..8d10828e7 100644 --- a/playkit/src/main/java/com/kaltura/playkit/profiler/OkHttpListener.java +++ b/playkit/src/main/java/com/kaltura/playkit/profiler/OkHttpListener.java @@ -2,9 +2,9 @@ import android.os.Build; import android.os.SystemClock; -import android.text.TextUtils; import com.kaltura.playkit.PKLog; +import com.kaltura.playkit.player.Profiler.Event; import java.io.IOException; import java.net.InetAddress; @@ -24,9 +24,6 @@ import okhttp3.Response; import okhttp3.TlsVersion; -import static com.kaltura.playkit.profiler.PlayKitProfiler.field; -import static com.kaltura.playkit.profiler.PlayKitProfiler.timeField; - class OkHttpListener extends EventListener { private static final PKLog log = PKLog.get("OkHttpListener"); @@ -42,23 +39,19 @@ class OkHttpListener extends EventListener { OkHttpListener(PlayKitProfiler playKitProfiler, Call call) { profiler = playKitProfiler; final Request request = call.request(); - if (request != null) { - HttpUrl httpUrl = request.url(); - if (httpUrl != null) { - hostName = httpUrl.host(); - url = httpUrl.toString(); - } else { - hostName = null; - url = null; - } - } else { - url = null; - hostName = null; - } + HttpUrl httpUrl = request.url(); + hostName = httpUrl.host(); + url = httpUrl.toString(); } - private void log(String event, String... strings) { - profiler.log("net_" + event, field("id", id), timeField("callTime", relTime()), TextUtils.join("\t", strings)); +// private void log(String event, String... strings) { +// profiler.log(event).add("id", id).addTime("callTime", relTime()), TextUtils.join("\t", strings)); +// } + + private Event logStart(String event) { + return (Event) profiler.logStart("net_" + event) + .add("id", id) + .addTime("callTime", relTime()); } private long relTime() { @@ -67,37 +60,39 @@ private long relTime() { @Override public void callStart(Call call) { - log.d("callStart " + id); - log("callStart", - field("url", url), field("hostName", hostName), - field("method", call.request().method())); + logStart("callStart") + .add("url", url) + .add("hostName", hostName) + .add("method", call.request().method()).end(); } @Override public void dnsStart(Call call, String domainName) { - log("dnsStart", field("hostName", domainName)); + logStart("dnsStart") + .add("hostName", domainName).end(); } @Override public void dnsEnd(Call call, String domainName, List inetAddressList) { if (!inetAddressList.isEmpty()) { final InetAddress address = inetAddressList.get(0); - log("dnsEnd", - field("hostName", domainName), - field("hostIp", address.getHostAddress()), - field("canonicalHostName", address.getCanonicalHostName())); + logStart("dnsEnd") + .add("hostName", domainName) + .add("hostIp", address.getHostAddress()) + .add("canonicalHostName", address.getCanonicalHostName()).end(); } else { - log("dnsEnd", field("hostName", domainName)); + logStart("dnsEnd") + .add("hostName", domainName).end(); } } @Override public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { - log("connectStart", - field("hostName", host(inetSocketAddress)), - field("hostIp", inetSocketAddress.getAddress().getHostAddress()), - field("port", inetSocketAddress.getPort()), - field("proxy", String.valueOf(proxy))); + logStart("connectStart") + .add("hostName", host(inetSocketAddress)) + .add("hostIp", inetSocketAddress.getAddress().getHostAddress()) + .add("port", inetSocketAddress.getPort()) + .add("proxy", String.valueOf(proxy)).end(); } private static String host(InetSocketAddress inetSocketAddress) { @@ -118,7 +113,7 @@ private static String host(Call call) { @Override public void secureConnectStart(Call call) { - log("secureConnectStart"); + logStart("secureConnectStart").end(); } @Override @@ -126,78 +121,87 @@ public void secureConnectEnd(Call call, Handshake handshake) { final CipherSuite cipherSuite = handshake.cipherSuite(); final TlsVersion tlsVersion = handshake.tlsVersion(); - log("secureConnectEnd", - field("cipherSuite", "" + cipherSuite), - field("tlsVersion", tlsVersion == null ? null : tlsVersion.javaName())); + logStart("secureConnectEnd") + .add("cipherSuite", "" + cipherSuite) + .add("tlsVersion", tlsVersion.javaName()) + .end(); } @Override public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) { - log("connectEnd", field("protocol", "" + protocol)); + logStart("connectEnd") + .add("protocol", "" + protocol) + .end(); } @Override public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) { - log("connectFailed", field("error", ioe.toString())); + logStart("connectFailed") + .add("error", ioe.toString()) + .end(); } @Override public void connectionAcquired(Call call, Connection connection) { - log("connectionAcquired"); + logStart("connectionAcquired").end(); } @Override public void connectionReleased(Call call, Connection connection) { - log("connectionReleased"); + logStart("connectionReleased").end(); } @Override public void requestHeadersStart(Call call) { - log("requestHeadersStart"); + logStart("requestHeadersStart").end(); } @Override public void requestHeadersEnd(Call call, Request request) { - log("requestHeadersEnd"); + logStart("requestHeadersEnd").end(); } @Override public void requestBodyStart(Call call) { - log("requestBodyStart"); + logStart("requestBodyStart").end(); } @Override public void requestBodyEnd(Call call, long byteCount) { - log("requestBodyEnd"); + logStart("requestBodyEnd").end(); } @Override public void responseHeadersStart(Call call) { - log("responseHeadersStart"); + logStart("responseHeadersStart").end(); } @Override public void responseHeadersEnd(Call call, Response response) { - log("responseHeadersEnd"); + logStart("responseHeadersEnd").end(); } @Override public void responseBodyStart(Call call) { - log("responseBodyStart"); + logStart("responseBodyStart").end(); } @Override public void responseBodyEnd(Call call, long byteCount) { - log("responseBodyEnd", field("byteCount", byteCount)); + logStart("responseBodyEnd") + .add("byteCount", byteCount) + .end(); } @Override public void callEnd(Call call) { - log("callEnd"); + logStart("callEnd").end(); } @Override public void callFailed(Call call, IOException ioe) { - log("callFailed", field("error", ioe.toString())); + logStart("callFailed") + .add("error", ioe) + .end(); } } diff --git a/playkit/src/main/java/com/kaltura/playkit/profiler/PlayKitProfiler.java b/playkit/src/main/java/com/kaltura/playkit/profiler/PlayKitProfiler.java index 876f46480..6571c9f97 100644 --- a/playkit/src/main/java/com/kaltura/playkit/profiler/PlayKitProfiler.java +++ b/playkit/src/main/java/com/kaltura/playkit/profiler/PlayKitProfiler.java @@ -2,37 +2,30 @@ import android.content.Context; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import android.os.SystemClock; -import android.text.TextUtils; import android.util.DisplayMetrics; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.gson.Gson; -import com.google.gson.JsonArray; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.kaltura.android.exoplayer2.C; import com.kaltura.android.exoplayer2.analytics.AnalyticsListener; -import com.kaltura.playkit.PKDrmParams; import com.kaltura.playkit.PKLog; import com.kaltura.playkit.PKMediaConfig; -import com.kaltura.playkit.PKMediaEntry; -import com.kaltura.playkit.PKMediaSource; import com.kaltura.playkit.PlayKitManager; +import com.kaltura.playkit.PlayKitManager.ProfilerConfig; import com.kaltura.playkit.Utils; import com.kaltura.playkit.player.ExoPlayerWrapper; -import com.kaltura.playkit.player.LoadControlBuffers; -import com.kaltura.playkit.player.MediaSupport; import com.kaltura.playkit.player.PKMediaSourceConfig; import com.kaltura.playkit.player.PlayerEngine; import com.kaltura.playkit.player.PlayerSettings; import com.kaltura.playkit.player.Profiler; +import com.kaltura.playkit.player.Profiler.Event; import com.kaltura.playkit.player.ProfilerFactory; import com.kaltura.playkit.utils.Consts; @@ -43,14 +36,9 @@ import java.lang.ref.WeakReference; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; @@ -58,32 +46,27 @@ public class PlayKitProfiler { + public static final String FORMAT_VERSION = "2.0"; + // Static constants private static final PKLog pkLog = PKLog.get("PlayKitProfiler"); // Dev mode: shorter logs, write to local file, always enable - private static final boolean devMode = false; - private static final int SEND_INTERVAL_DEV = 60; // in seconds + private static final boolean devMode = true; + private static final int SEND_INTERVAL_DEV = 30; // sec private static final int SEND_PERCENTAGE_DEV = 100; // always - private static final int SEND_INTERVAL_PROD = 120; // 2 minutes + private static final int SEND_INTERVAL_PROD = 60; // sec private static final int SEND_INTERVAL_SEC = devMode ? SEND_INTERVAL_DEV : SEND_INTERVAL_PROD; private static final float DEFAULT_SEND_PERCENTAGE = devMode ? SEND_PERCENTAGE_DEV : 0; // Start disabled - private static final String CONFIG_CACHE_FILENAME = "profilerConfig.json"; - private static final String CONFIG_BASE_URL = "https://s3.amazonaws.com/player-profiler/configs/"; - private static final int MAX_CONFIG_SIZE = 10240; - static final float MSEC_MULTIPLIER_FLOAT = 1000f; - private static final String SEPARATOR = "\t"; - - private static final Map experiments = new LinkedHashMap<>(); + private static final JsonObject experiments = new JsonObject(); private static final int PERCENTAGE_MULTIPLIER = 100; // Configuration - private static String configToken; - private static String postURL; + private static String postURL = "https://dtvqq1tbxf.execute-api.us-east-1.amazonaws.com/default/profilerLogCollector"; private static float sendPercentage = DEFAULT_SEND_PERCENTAGE; // Static setup private static Handler ioHandler; @@ -106,6 +89,8 @@ public class PlayKitProfiler { @Nullable private WeakReference playerEngine; + private Profiler profilerImp; + private PlayKitProfiler() { ioHandler.post(new Runnable() { @@ -125,13 +110,13 @@ public void run() { * Initialize the static part of the profiler -- load the config and store it, * create IO thread and handler. Must be called by the app to enable the profiler. */ - public static void init(Context context, String jsonConfigToken) { + public static void init(Context context, ProfilerConfig config) { // This only has to happen once. if (initialized) { return; } - configToken = jsonConfigToken; + synchronized (PlayKitProfiler.class) { // Ask again, after sync. @@ -145,7 +130,7 @@ public static void init(Context context, String jsonConfigToken) { handlerThread.start(); ioHandler = new Handler(handlerThread.getLooper()); - ioHandler.post(() -> downloadConfig(appContext)); + ioHandler.post(() -> applyConfig(config)); initMembers(appContext); @@ -161,8 +146,15 @@ public static void init(Context context, String jsonConfigToken) { // random number is smaller than 0.05 a profiler is created and returned. // As a result, 5 in every 100 calls to the factory will create a real profiler. - ProfilerFactory.setFactory(() -> - Math.random() < sendPercentage / PERCENTAGE_MULTIPLIER ? new PlayKitProfiler().profilerImp : null); + ProfilerFactory.setFactory(() -> { + final double random = Math.random(); + final boolean enable = random < sendPercentage / PERCENTAGE_MULTIPLIER; + pkLog.d("Profiler enabled for session? " + enable); + if (enable) { + return new PlayKitProfiler().getProfilerImp(); + } + return null; + }); } } @@ -174,38 +166,26 @@ public static void init(Context context, String jsonConfigToken) { * @param key A unique string that describes the property being tested * @param value the value of the key, must be a string, a number or a boolean. */ - public static void setExperiment(String key, Object value) { - if (key == null) { - pkLog.w("setExperiment: key is null"); - return; - } - - final String strValue; - if (value instanceof String) { - strValue = "{" + value + "}"; - } else if (value instanceof Number || value instanceof Boolean) { - strValue = value.toString(); + public static void setExperiment(@NonNull String key, @Nullable Object value) { + + if (value instanceof Number) { + experiments.addProperty(key, (Number) value); + } else if (value instanceof Boolean) { + experiments.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + experiments.addProperty(key, (Character) value); + } else if (value == null) { + experiments.add(key, JsonNull.INSTANCE); } else { - pkLog.w("setExperiment: value type is not valid (" + (value != null ? value.getClass().toString() : null) + "); ignored"); - return; + // Anything else (including a String) is used as a string + experiments.addProperty(key, value.toString()); } - - experiments.put(key, strValue); } private void logExperiments() { - - List values = new ArrayList<>(); - - for (Map.Entry entry : experiments.entrySet()) { - - final String key = entry.getKey(); - final String value = entry.getValue(); - - values.add(key + "=" + value); - } - - log("Experiments", TextUtils.join("\t", values)); + logStart("Experiments") + .addAll(experiments) + .end(); } private static void initMembers(final Context context) { @@ -216,125 +196,14 @@ private static void initMembers(final Context context) { networkType = Utils.getNetworkClass(context); deviceType = Utils.getDeviceType(context); - if (devMode) { externalFilesDir = context.getExternalFilesDir(null); } } - private static String toString(Enum e) { - if (e == null) { - return "null"; - } - return e.name(); - } - - private static JsonObject toJSON(PKMediaEntry entry) { - - if (entry == null) { - return null; - } - - JsonObject json = new JsonObject(); - - json.addProperty("id", entry.getId()); - json.addProperty("duration", entry.getDuration()); - json.addProperty("type", toString(entry.getMediaType())); - - if (entry.hasSources()) { - JsonArray array = new JsonArray(); - for (PKMediaSource source : entry.getSources()) { - array.add(toJSON(source)); - } - json.add("sources", array); - } - - return json; - } - - private static JsonObject toJSON(PKMediaSource source) { - JsonObject json = new JsonObject(); - - json.addProperty("id", source.getId()); - json.addProperty("format", source.getMediaFormat().name()); - json.addProperty("url", source.getUrl()); - - if (source.hasDrmParams()) { - JsonArray array = new JsonArray(); - for (PKDrmParams params : source.getDrmData()) { - PKDrmParams.Scheme scheme = params.getScheme(); - if (scheme != null) { - array.add(scheme.name()); - } - } - json.add("drm", array); - } - - return json; - } - - static String field(String name, String value) { - if (value == null) { - return null; - } - return name + "={" + value + "}"; - } - - static String field(String name, long value) { - return name + "=" + value; - } - - static String field(String name, boolean value) { - return name + "=" + value; - } - - static String field(String name, float value) { - return String.format(Locale.US, "%s=%.03f", name, value); - } - - static String timeField(String name, long value) { - return value == C.TIME_UNSET ? field(name, null) : field(name, value / MSEC_MULTIPLIER_FLOAT); - } - - private static void downloadConfig(Context context) { - final byte[] bytes; - - // Download - try { - bytes = Utils.executeGet(CONFIG_BASE_URL + configToken + ".json", null); - - if (bytes == null || bytes.length == 0) { - pkLog.w("Nothing returned from executeGet"); - return; - } - - parseConfig(bytes); - - } catch (IOException e) { - pkLog.w("Failed to download config", e); - } - } - - private static void parseConfig(byte[] bytes) { - try { - final ConfigFile configFile = new Gson().fromJson(new String(bytes), ConfigFile.class); - postURL = configFile.postURL; - sendPercentage = configFile.sendPercentage; - } catch (JsonParseException e) { - pkLog.e("Failed to parse config", e); - } - } - - static String nullable(String name, String value) { - if (value == null) { - return name + "=null"; - } - - return field(name, value); - } - - static String joinFields(String... fields) { - return TextUtils.join(SEPARATOR, fields); + private static void applyConfig(ProfilerConfig config) { + postURL = config.postURL; + sendPercentage = config.sendPercentage; } private void sendLogChunk() { @@ -391,13 +260,19 @@ private void sendLogChunk() { } private void postChunk(String string, int chunkIndex) { + if (postURL == null) { pkLog.w("No POST URL"); return; } try { - Utils.executePost(postURL + "?mode=addChunk&sessionId=" + sessionId + "&index=" + chunkIndex, string.getBytes(), null); + final String url = postURL + "?sessionId=" + sessionId + "&index=" + chunkIndex; + if (pkLog.isLoggable(PKLog.Level.verbose)) { + pkLog.v("POST to " + url); + pkLog.v(string); + } + Utils.executePost(url, string.getBytes(), null); } catch (IOException e) { // FIXME: 03/09/2018 Is it bad that we lost this log chunk? pkLog.e("Failed sending log", e); @@ -405,45 +280,21 @@ private void postChunk(String string, int chunkIndex) { } } - void log(String event, String... strings) { - StringBuilder sb = startLog(event); - logPayload(sb, strings); - endLog(sb); + public long timestamp() { + return SystemClock.elapsedRealtime() - sessionStartTime; } - private StringBuilder startLog(String event) { - - // Pre-allocate the string to something reasonable - StringBuilder sb = new StringBuilder(100); - sb - .append(SystemClock.elapsedRealtime() - sessionStartTime) - .append(SEPARATOR) - .append(event); - - return sb; + public Event logStart(String name) { + return new Event(this, name); } - private void logPayload(StringBuilder sb, String... strings) { - for (String s : strings) { - if (s == null) { - continue; - } - sb.append(SEPARATOR).append(s); + public Event logStart(String name, @Nullable PlayerEngine playerEngine) { + final Event event = logStart(name); + if (playerEngine != null) { + event.addTime("pos", playerEngine.getCurrentPosition()); + event.addTime("buf", playerEngine.getBufferedPosition()); } - } - - private void endLog(final StringBuilder sb) { - logQueue.add(sb.toString()); - } - - private void logWithPlaybackInfo(String event, PlayerEngine playerEngine, String... strings) { - - StringBuilder sb = startLog(event); - - logPayload(sb, timeField("pos", playerEngine.getCurrentPosition()), timeField("buf", playerEngine.getBufferedPosition())); - logPayload(sb, strings); - - endLog(sb); + return event; } void maybeLogServerInfo(final Uri url) { @@ -476,13 +327,12 @@ private void logServerInfo(String hostName) { error = e.toString(); } - log("ServerInfo", - field("hostName", hostName), - field("canonicalHostName", canonicalHostName), - field("hostIp", hostIp), - timeField("lookupTime", lookupTime), - field("lookupError", error) - ); + logStart("ServerInfo") + .add("hostName", hostName) + .add("canonicalHostName", canonicalHostName) + .add("hostIp", hostIp) + .addTime("lookupTime", lookupTime) + .add("lookupError", error).end(); serversLookedUp.add(hostName); } @@ -492,164 +342,165 @@ private void closeSession() { sendLogChunk(); } - void logWithPlaybackInfo(String event, String... strings) { - if (playerEngine != null) { - logWithPlaybackInfo(event, playerEngine.get(), strings); + Event logWithPlaybackInfo(String event) { + return logStart(event, playerEngine != null ? playerEngine.get() : null); + } + + private Profiler getProfilerImp() { + if (profilerImp == null) { + profilerImp = createProfilerImp(); } + + return profilerImp; } - private Profiler profilerImp = new Profiler() { - @Override - public void setPlayerEngine(PlayerEngine engine) { + private Profiler createProfilerImp() { - if (engine instanceof ExoPlayerWrapper) { - playerEngine = new WeakReference<>(((ExoPlayerWrapper) engine)); - } else { - playerEngine = null; // other engines are not supported - } - } + pkLog.d("Creating a real profiler"); - @Override - public void onPrepareStarted(final PKMediaSourceConfig sourceConfig) { + return new Profiler() { + @Override + public void setPlayerEngine(PlayerEngine engine) { - final Uri sourceUrl = sourceConfig.getRequestParams().url; + if (engine instanceof ExoPlayerWrapper) { + playerEngine = new WeakReference<>(((ExoPlayerWrapper) engine)); + } else { + playerEngine = null; // other engines are not supported + } + } - log("PrepareStarted", - field("engine", playerEngine.get().getClass().getSimpleName()), - field("source", sourceUrl.toString())); + @Override + public void onPrepareStarted(final PKMediaSourceConfig sourceConfig) { - maybeLogServerInfo(sourceUrl); - } + final Uri sourceUrl = sourceConfig.getRequestParams().url; - @Override - public void newSession(final String sessionId, PlayerSettings playerSettings) { + logStart("PrepareStarted") + .add("engine", "ExoPlayer") + .add("source", sourceUrl.toString()).end(); - if (sessionId != null) { - // close current session - closeSession(); + maybeLogServerInfo(sourceUrl); } - PlayKitProfiler.this.sessionId = sessionId; - PlayKitProfiler.this.chunkCount = 0; - if (sessionId == null) { - return; // the null profiler - } + @Override + public void newSession(final String sessionId, PlayerSettings playerSettings) { - PlayKitProfiler.this.sessionStartTime = SystemClock.elapsedRealtime(); - PlayKitProfiler.this.logQueue.clear(); - - PlayKitProfiler.this.serversLookedUp.clear(); - - pkLog.d("New profiler with sessionId: " + sessionId); - - log("StartSession", - field("now", System.currentTimeMillis()), - field("strNow", new Date().toString()), - field("sessionId", sessionId), - field("packageName", packageName) - ); - - log("PlayKit", - field("version", PlayKitManager.VERSION_STRING), - field("clientTag", PlayKitManager.CLIENT_TAG) - ); - - log("Platform", - field("name", "Android"), - field("apiLevel", Build.VERSION.SDK_INT), - field("chipset", MediaSupport.DEVICE_CHIPSET), - field("brand", Build.BRAND), - field("model", Build.MODEL), - field("manufacturer", Build.MANUFACTURER), - field("device", Build.DEVICE), - field("tags", Build.TAGS), - field("fingerprint", Build.FINGERPRINT), - field("screenSize", metrics.widthPixels + "x" + metrics.heightPixels), - field("screenDpi", metrics.xdpi + "x" + metrics.ydpi), - field("deviceType", deviceType), - field("networkType", networkType) - ); - - log("PlayerSettings", - field("allowClearLead", playerSettings.allowClearLead()), - field("useTextureView", playerSettings.useTextureView())); - - final LoadControlBuffers loadControl = playerSettings.getLoadControlBuffers(); - if (loadControl != null) { - log("PlayerLoadControl", - field("minBufferLenMs", loadControl.getMinPlayerBufferMs()), - field("maxBufferLenMs", loadControl.getMaxPlayerBufferMs()), - field("minRebufferLenMs", loadControl.getMinBufferAfterReBufferMs()), - field("minSeekBufferLenMs", loadControl.getMinBufferAfterInteractionMs()) - ); - } + pkLog.d("New session " + sessionId); + if (PlayKitProfiler.this.sessionId != null) { + // close current session + closeSession(); + } - logExperiments(); - } + PlayKitProfiler.this.sessionId = sessionId; + PlayKitProfiler.this.chunkCount = 0; + if (sessionId == null) { + return; // the null profiler + } - @Override - public AnalyticsListener getExoAnalyticsListener() { - return analyticsListener; - } + PlayKitProfiler.this.sessionStartTime = SystemClock.elapsedRealtime(); + PlayKitProfiler.this.logQueue.clear(); - @Override - public void onApplicationPaused() { - log("ApplicationPaused"); - } + PlayKitProfiler.this.serversLookedUp.clear(); - @Override - public void onApplicationResumed() { - log("onApplicationResumed"); - } + pkLog.d("New profiler with sessionId: " + sessionId); - @Override - public void onSetMedia(PKMediaConfig mediaConfig) { - JsonObject json = new JsonObject(); - json.add("entry", toJSON(mediaConfig.getMediaEntry())); - json.addProperty("startPosition", mediaConfig.getStartPosition()); + logStart("StartSession") + .add("now", System.currentTimeMillis()) + .add("strNow", new Date().toString()) + .add("sessionId", sessionId) + .add("appId", packageName) + .add("version", FORMAT_VERSION) + .end(); - log("SetMedia", field("config", json.toString())); - } + logStart("PlayKit") + .add("version", PlayKitManager.VERSION_STRING) + .add("clientTag", PlayKitManager.CLIENT_TAG) + .add("type", "sdk").end(); - @Override - public void onSeekRequested(long position) { - logWithPlaybackInfo("SeekRequested", timeField("targetPosition", position)); - } + logStart("Platform") + .add("os", "Android") + .add("screenSize", metrics.widthPixels + "x" + metrics.heightPixels) + .add("screenDpi", metrics.xdpi + "x" + metrics.ydpi) + .add("deviceType", deviceType) + .add("networkType", networkType).end(); - @Override - public void onPauseRequested() { - logWithPlaybackInfo("PauseRequested"); - } + logStart("AndroidInfo") + .addAll(ToJson.buildInfoJson()) + .end(); - @Override - public void onReplayRequested() { - logWithPlaybackInfo("ReplayRequested"); - } + logStart("PlayerSettings") + .addAll(ToJson.toJson(playerSettings)) + .end(); - @Override - public void onPlayRequested() { - logWithPlaybackInfo("PlayRequested"); - } + logExperiments(); + } - @Override - public void onSessionFinished() { - closeSession(); - } + @Override + public AnalyticsListener getExoAnalyticsListener() { + return analyticsListener; + } - @Override - public void onDurationChanged(long duration) { - log("DurationChanged", timeField("duration", duration)); - } + @Override + public void onApplicationPaused() { + logStart("ApplicationPaused").end(); + } - @Override - public EventListener.Factory getOkListenerFactory() { - return okListenerFactory; - } - }; + @Override + public void onApplicationResumed() { + logStart("ApplicationResumed").end(); + } + + @Override + public void onSetMedia(PKMediaConfig mediaConfig) { + + logStart("SetMedia") + .add("entry", ToJson.toJson(mediaConfig.getMediaEntry())) + .add("startPosition", mediaConfig.getStartPosition()) + .end(); + } + + @Override + public void onSeekRequested(long position) { + logWithPlaybackInfo("SeekRequested") + .addTime("targetPosition", position) + .end(); + } + + @Override + public void onPauseRequested() { + logWithPlaybackInfo("PauseRequested").end(); + } + + @Override + public void onReplayRequested() { + logWithPlaybackInfo("ReplayRequested").end(); + } + + @Override + public void onPlayRequested() { + logWithPlaybackInfo("PlayRequested").end(); + } + + @Override + public void onSessionFinished() { + closeSession(); + } + + @Override + public void onDurationChanged(long duration) { + logStart("DurationChanged") + .addTime("duration", duration) + .end(); + } + + @Override + public EventListener.Factory getOkListenerFactory() { + return okListenerFactory; + } + }; + } - private static class ConfigFile { - String postURL; - float sendPercentage; + public void append(JsonObject jo) { + logQueue.add(jo.toString()); } } diff --git a/playkit/src/main/java/com/kaltura/playkit/profiler/ToJson.java b/playkit/src/main/java/com/kaltura/playkit/profiler/ToJson.java new file mode 100644 index 000000000..7df719e31 --- /dev/null +++ b/playkit/src/main/java/com/kaltura/playkit/profiler/ToJson.java @@ -0,0 +1,228 @@ +package com.kaltura.playkit.profiler; + +import android.graphics.Typeface; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.gson.JsonArray; +import com.kaltura.android.exoplayer2.ui.CaptionStyleCompat; +import com.kaltura.playkit.PKDrmParams; +import com.kaltura.playkit.PKMediaEntry; +import com.kaltura.playkit.PKMediaSource; +import com.kaltura.playkit.PKTrackConfig; +import com.kaltura.playkit.Utils.GsonObject; +import com.kaltura.playkit.player.ABRSettings; +import com.kaltura.playkit.player.LoadControlBuffers; +import com.kaltura.playkit.player.MediaSupport; +import com.kaltura.playkit.player.PKSubtitlePosition; +import com.kaltura.playkit.player.PlayerSettings; +import com.kaltura.playkit.player.SubtitleStyleSettings; +import com.kaltura.playkit.player.vr.VRSettings; + +public class ToJson { + @Nullable + static GsonObject toJson(@Nullable PKMediaEntry entry) { + + if (entry == null) { + return null; + } + + JsonArray sources = new JsonArray(); + if (entry.hasSources()) { + for (PKMediaSource source : entry.getSources()) { + sources.add(toJson(source).jsonObject()); + } + } + + return new GsonObject() + .add("id", entry.getId()) + .add("duration", entry.getDuration()) + .add("type", String.valueOf(entry.getMediaType())) + .add("sources", sources); + } + + @NonNull + static GsonObject toJson(@Nullable PKMediaSource source) { + if (source == null) { + return new GsonObject(); + } + JsonArray drmParams = new JsonArray(); + if (source.hasDrmParams()) { + for (PKDrmParams params : source.getDrmData()) { + PKDrmParams.Scheme scheme = params.getScheme(); + if (scheme != null) { + drmParams.add(scheme.name()); + } + } + } + + return new GsonObject() + .add("id", source.getId()) + .add("format", source.getMediaFormat().name()) + .add("url", source.getUrl()) + .add("drm", drmParams); + } + + @Nullable + static GsonObject toJson(@Nullable ABRSettings s) { + if (s == null) { + return null; + } + return new GsonObject() + .add("initialBitrateEstimate", s.getInitialBitrateEstimate()) + .add("maxVideoBitrate", s.getMaxVideoBitrate()) + .add("minVideoBitrate", s.getMinVideoBitrate()); + } + + @Nullable + static GsonObject toJson(@Nullable VRSettings s) { + if (s == null) { + return null; + } + return new GsonObject() + .add("flingEnabled", s.isFlingEnabled()) + .add("vrModeEnabled", s.isVrModeEnabled()) + .add("zoomWithPinchEnabled", s.isZoomWithPinchEnabled()) + .add("interactionMode", s.getInteractionMode().name()); + } + + @Nullable + static GsonObject toJson(@Nullable SubtitleStyleSettings s) { + if (s == null) { + return null; + } + return new GsonObject() + .add("textColor", Integer.toHexString(s.getTextColor())) + .add("backgroundColor", Integer.toHexString(s.getBackgroundColor())) + .add("textSizeFraction", s.getTextSizeFraction()) + .add("windowColor", Integer.toHexString(s.getWindowColor())) + .add("edgeType", edgeTypeName(s.getEdgeType())) + .add("edgeColor", Integer.toHexString(s.getEdgeColor())) + .add("typeface", toJson(s.getTypeface())) + .add("styleName", s.getStyleName()) + .add("position", toJson(s.getSubtitlePosition())); + } + + private static String edgeTypeName(@CaptionStyleCompat.EdgeType int edgeType) { + switch (edgeType) { + case CaptionStyleCompat.EDGE_TYPE_NONE: + return "none"; + case CaptionStyleCompat.EDGE_TYPE_OUTLINE: + return "outline"; + case CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW: + return "dropShadow"; + case CaptionStyleCompat.EDGE_TYPE_RAISED: + return "raised"; + case CaptionStyleCompat.EDGE_TYPE_DEPRESSED: + return "depressed"; + default: + return "unknown"; + } + } + + @Nullable + static GsonObject toJson(@Nullable PKSubtitlePosition s) { + if (s == null) { + return null; + } + return new GsonObject() + .add("horizontalPositionPercentage", s.getHorizontalPositionPercentage()) + .add("verticalPositionPercentage", s.getVerticalPositionPercentage()) + .add("overrideInlineCueConfig", s.isOverrideInlineCueConfig()) + .add("horizontalAlignment", s.getSubtitleHorizontalPosition().name()); + } + + @Nullable + static GsonObject toJson(@Nullable Typeface s) { + if (s == null) { + return null; + } + final GsonObject object = new GsonObject() + .add("bold", s.isBold()) + .add("italic", s.isItalic()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + object.add("weight", s.getWeight()); + } + + return object; + } + + @Nullable + static GsonObject toJson(@Nullable PKTrackConfig s) { + if (s == null) { + return null; + } + return new GsonObject() + .add("preferredMode", s.getPreferredMode().name()) + .add("language", s.getTrackLanguage()); + } + + @Nullable + static GsonObject toJson(@Nullable Long height, Long width) { + if (height == null || width == null) { + return null; + } + return new GsonObject() + .add("width", height) + .add("height", width); + } + + @Nullable + static GsonObject toJson(@Nullable LoadControlBuffers s) { + if (s == null) { + return null; + } + return new GsonObject() + .addTime("minBufferSec", s.getMinPlayerBufferMs()) + .addTime("maxBufferSec", s.getMaxPlayerBufferMs()) + .addTime("minBufferAfterReBufferSec", s.getMinBufferAfterReBufferMs()) + .addTime("minBufferAfterInteractionSec", s.getMinBufferAfterInteractionMs()) + .addTime("backBufferDurationSec", s.getBackBufferDurationMs()) + .addTime("allowedVideoJoiningTimeSec", s.getAllowedVideoJoiningTimeMs()) + .add("retainBackBufferFromKeyframe", s.getRetainBackBufferFromKeyframe()); + } + + @Nullable + static GsonObject toJson(@Nullable PlayerSettings settings) { + if (settings == null) { + return null; + } + return new GsonObject() + .add("clearLead", settings.allowClearLead()) + .add("cea608Captions", settings.cea608CaptionsEnabled()) + .add("decoderFallback", settings.enableDecoderFallback()) + .add("abrSettings", toJson(settings.getAbrSettings())) + .add("aspectRatioResizeMode", settings.getAspectRatioResizeMode().name()) + .add("loadControlBuffers", toJson(settings.getLoadControlBuffers())) + .add("maxAudioBitrate", settings.getMaxAudioBitrate()) + .add("maxAudioChannelCount", settings.getMaxAudioChannelCount()) + .add("maxVideoBitrate", settings.getAbrSettings().getMaxVideoBitrate()) + .add("maxVideoSize", toJson(settings.getAbrSettings().getMaxVideoHeight(), settings.getAbrSettings().getMaxVideoWidth())) + .add("preferredAudioTrack", toJson(settings.getPreferredAudioTrackConfig())) + .add("preferredMediaFormat", settings.getPreferredMediaFormat().name()) + .add("preferredTextTrack", toJson(settings.getPreferredTextTrackConfig())) + .add("subtitleStyle", toJson(settings.getSubtitleStyleSettings())) + .add("vr", toJson(settings.getVRSettings())) + .add("adAutoPlayOnResume", settings.isAdAutoPlayOnResume()) + .add("forceSinglePlayerEngine", settings.isForceSinglePlayerEngine()) + .add("handleAudioBecomingNoisy", settings.isHandleAudioBecomingNoisyEnabled()) + .add("secureSurface", settings.isSurfaceSecured()) + .add("tunneledAudioPlayback", settings.isTunneledAudioPlayback()) + .add("vrEnabled", settings.isVRPlayerEnabled()); + } + + static GsonObject buildInfoJson() { + return new GsonObject() + .add("apiLevel", Build.VERSION.SDK_INT) + .add("chipset", MediaSupport.DEVICE_CHIPSET) + .add("brand", Build.BRAND) + .add("model", Build.MODEL) + .add("manufacturer", Build.MANUFACTURER) + .add("device", Build.DEVICE) + .add("tags", Build.TAGS) + .add("fingerprint", Build.FINGERPRINT); + } +}