/* * Copyright 2014 RobustNet Lab, University of Michigan. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.mobilyzer.measurements; import java.io.InvalidClassException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Map; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.MeasurementTask; import com.mobilyzer.UpdateIntent; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; import com.mobilyzer.util.MeasurementJsonConvertor; import com.mobilyzer.util.PhoneUtils; import com.mobilyzer.util.video.VideoPlayerService; import com.mobilyzer.util.video.util.DemoUtil; /** * @author laoyao * Measure the user-perceived Video QoE metrics by playing YouTube video in the background */ public class VideoQoETask extends MeasurementTask { // Type name for internal use public static final String TYPE = "video"; // Human readable name for the task public static final String DESCRIPTOR = "Video QoE"; private boolean isSucceed = false; private int numFrameDropped; private double initialLoadingTime; private ArrayList<Double> rebufferTimes = new ArrayList<Double>(); private ArrayList<String> goodputTimestamps = new ArrayList<String>(); private ArrayList<Double> goodputValues = new ArrayList<Double>(); private ArrayList<Long> goodputEstimateValues = new ArrayList<Long>(); private ArrayList<String> bitrateTimestamps = new ArrayList<String>(); private ArrayList<Integer> bitrateValues = new ArrayList<Integer>(); private long bbaSwitchTime; private long dataConsumed; private boolean isResultReceived; private long duration; /** * @author laoyao * Parameters for Video QoE measurement */ public static class VideoQoEDesc extends MeasurementDesc { // The url to retrieve video public String contentURL; // The content id for YouTube video public String contentId; // The ABR algorithm for video playback public int contentType; public VideoQoEDesc(String key, Date startTime, Date endTime, double intervalSec, long count, long priority, int contextIntervalSec, Map<String, String> params) { super(VideoQoETask.TYPE, key, startTime, endTime, intervalSec, count, priority, contextIntervalSec, params); initializeParams(params); if (this.contentURL == null) { throw new InvalidParameterException("Video QoE task cannot be created" + " due to null video url string"); } if (this.contentType != DemoUtil.TYPE_DASH_VOD && this.contentType != DemoUtil.TYPE_PROGRESSIVE && this.contentType != DemoUtil.TYPE_BBA ) { throw new InvalidParameterException("Video QoE task cannot be created" + " due to invalid streaming algorithm: " + this.contentType); } } @Override public String getType() { return VideoQoETask.TYPE; } @Override protected void initializeParams(Map<String, String> params) { if (params == null) { return; } String val = null; this.contentURL = params.get("content_url"); this.contentId = params.get("content_id"); if ((val = params.get("content_type")) != null && Integer.parseInt(val) >= 0) { this.contentType = Integer.parseInt(val); } } protected VideoQoEDesc(Parcel in) { super(in); contentURL = in.readString(); contentId = in.readString(); contentType = in.readInt(); } public static final Parcelable.Creator<VideoQoEDesc> CREATOR = new Parcelable.Creator<VideoQoEDesc>() { public VideoQoEDesc createFromParcel(Parcel in) { return new VideoQoEDesc(in); } public VideoQoEDesc[] newArray(int size) { return new VideoQoEDesc[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(this.contentURL); dest.writeString(this.contentId); dest.writeInt(this.contentType); } } /** * Constructor for video QoE measuremen task * @param desc */ public VideoQoETask(MeasurementDesc desc) { super(new VideoQoEDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); bbaSwitchTime=-1; dataConsumed=0; } protected VideoQoETask(Parcel in) { super(in); } public static final Parcelable.Creator<VideoQoETask> CREATOR = new Parcelable.Creator<VideoQoETask>() { public VideoQoETask createFromParcel(Parcel in) { return new VideoQoETask(in); } public VideoQoETask[] newArray(int size) { return new VideoQoETask[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#clone() */ @Override public MeasurementTask clone() { MeasurementDesc desc = this.measurementDesc; VideoQoEDesc newDesc = new VideoQoEDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); return new VideoQoETask(newDesc); } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#call() */ @Override public MeasurementResult[] call() throws MeasurementError { Logger.d("Video QoE: measurement started"); MeasurementResult[] mrArray = new MeasurementResult[1]; VideoQoEDesc taskDesc = (VideoQoEDesc) this.measurementDesc; Intent videoIntent = new Intent(PhoneUtils.getGlobalContext(), VideoPlayerService.class); videoIntent.setData(Uri.parse(taskDesc.contentURL)); videoIntent.putExtra(DemoUtil.CONTENT_ID_EXTRA, taskDesc.contentId); videoIntent.putExtra(DemoUtil.CONTENT_TYPE_EXTRA, taskDesc.contentType); PhoneUtils.getGlobalContext().startService(videoIntent); IntentFilter filter = new IntentFilter(); filter.addAction(UpdateIntent.VIDEO_MEASUREMENT_ACTION); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Logger.d("Video QoE: result received"); if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED)){ isSucceed = intent.getBooleanExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED, false); Logger.d("Is succeed: " + isSucceed); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED)){ numFrameDropped = intent.getIntExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED, 0); Logger.d("Num frame dropped: " + numFrameDropped); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME)){ initialLoadingTime = intent.getDoubleExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME, 0.0); Logger.d("Initial Loading Time: " + initialLoadingTime); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME)) { double[] rebufferTimeArray = intent.getDoubleArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME); for (double rebuffer : rebufferTimeArray) { rebufferTimes.add(rebuffer); } Logger.d("Rebuffer Times: " + rebufferTimes); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP)) { String[] goodputTimestampArray = intent.getStringArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP); goodputTimestamps = new ArrayList<String>(Arrays.asList(goodputTimestampArray)); Logger.d("Goodput Timestamps: " + goodputTimestamps); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE)) { double[] goodputValueArray = intent.getDoubleArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE); for (double goodput : goodputValueArray) { goodputValues.add(goodput); } Logger.d("Goodput Values: " + goodputValues); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE)) { long[] goodputEstimateValueArray = intent.getLongArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE); for (long estiamte : goodputEstimateValueArray) { goodputEstimateValues.add(estiamte); } Logger.d("Goodput Estimated Values: " + goodputValues); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP)) { String[] bitrateTimestampArray = intent.getStringArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP); bitrateTimestamps = new ArrayList<String>(Arrays.asList(bitrateTimestampArray)); Logger.d("Bitrate Timestamps: " + bitrateTimestamps); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE)) { int[] bitrateValueArray = intent.getIntArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE); for (int bitrate : bitrateValueArray) { bitrateValues.add(bitrate); } Logger.d("Bitrate Values: " + bitrateValues); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME)){ bbaSwitchTime=intent.getLongExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME, -1); } if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BYTE_USED)){ dataConsumed=intent.getLongExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BYTE_USED, 0); Logger.d("Data consumed: " + dataConsumed); } isResultReceived = true; } }; PhoneUtils.getGlobalContext().registerReceiver(broadcastReceiver, filter); for(int i=0;i<60*5;i++){ if(isDone()){ break; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } Logger.e("Video QoE: result ready? " + this.isResultReceived); PhoneUtils.getGlobalContext().unregisterReceiver(broadcastReceiver); if(isDone()){ Logger.i("Video QoE: Successfully measured QoE data"); PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); MeasurementResult result = new MeasurementResult( phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), VideoQoETask.TYPE, System.currentTimeMillis() * 1000, TaskProgress.COMPLETED, this.measurementDesc); // result.addResult(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED, isSucceed); result.addResult("video_num_frame_dropped", this.numFrameDropped); result.addResult("video_initial_loading_time", this.initialLoadingTime); result.addResult("video_rebuffer_times", this.rebufferTimes); result.addResult("video_goodput_times", this.goodputTimestamps); result.addResult("video_goodput_values", this.goodputValues); result.addResult("video_goodput_estimate_values", this.goodputEstimateValues); result.addResult("video_bitrate_times", this.bitrateTimestamps); result.addResult("video_bitrate_values", this.bitrateValues); if(this.bbaSwitchTime!=-1){ result.addResult("video_bba_switch_time", this.bbaSwitchTime); } Logger.i(MeasurementJsonConvertor.toJsonString(result)); mrArray[0]=result; }else{ Logger.i("Video QoE: Video measurement not finished"); PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); MeasurementResult result = new MeasurementResult( phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), VideoQoETask.TYPE, System.currentTimeMillis() * 1000, TaskProgress.FAILED, this.measurementDesc); // result.addResult("error", "measurement timeout"); Logger.i(MeasurementJsonConvertor.toJsonString(result)); mrArray[0]=result; } // PhoneUtils.getGlobalContext().stopService(new Intent(PhoneUtils.getGlobalContext(), PLTExecutorService.class)); return mrArray; } private boolean isDone() { return isResultReceived; } @SuppressWarnings("rawtypes") public static Class getDescClass() throws InvalidClassException { return VideoQoEDesc.class; } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#getDescriptor() */ @Override public String getDescriptor() { return VideoQoETask.DESCRIPTOR; } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#getType() */ @Override public String getType() { return VideoQoETask.TYPE; } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#stop() */ @Override public boolean stop() { // There is nothing we need to do to stop the video measurement return false; } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#getDuration() */ @Override public long getDuration() { return this.duration; } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#setDuration(long) */ @Override public void setDuration(long newDuration) { if (newDuration < 0) { this.duration = 0; } else { this.duration = newDuration; } } /* (non-Javadoc) * @see com.mobilyzer.MeasurementTask#getDataConsumed() */ @Override public long getDataConsumed() { return dataConsumed; } }