집요정 도비의 일기
Apply Digest Auth to Exoplayer 본문
I was doing a project that consisted of getting streams from AXIS web cams.
I had two functions to implement ; First, play the live stream, Second, play a portion of the live stream's history.
The AXIS web cam uses digest auth for authenticating it's stream.
The first step was easily done by putting the stream link inside a webview.
http://${userName}:${password}@${ip:port}/axis-cgi/mjpg/video.cgi?camera=1
But, the second approach did not work like this. Unlike the first link, the link I obtained for playing it's history was a file link. So, instead of messing around with webview for this, I decided to try out exoplayer. But, as far as I researched, exoplayer did not automatically authenticate digest auths. So, had to find a workaround.
The main idea of this approach is authenticating the connection via a seperate httpUrlConnection and applying the header obtained by it.
I found the below code from someone's gist.
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
public class HttpDigestAuth {
public HttpURLConnection tryAuth(HttpURLConnection connection, String username, String password)
throws IOException
{
int responseCode = connection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_UNAUTHORIZED){
connection = tryDigestAuthentication(connection, username, password);
if(connection == null){
throw new AuthenticationException();
}
}
return connection;
}
public static HttpURLConnection tryDigestAuthentication(HttpURLConnection input, String username, String password)
{
String auth = input.getHeaderField("WWW-Authenticate");
if(auth == null || !auth.startsWith("Digest ")){
return null;
}
final HashMap<String, String> authFields = splitAuthFields(auth.substring(7));
MessageDigest md5 = null;
try{
md5 = MessageDigest.getInstance("MD5");
}
catch(NoSuchAlgorithmException e){
return null;
}
Joiner colonJoiner = Joiner.on(':');
String HA1 = null;
try{
md5.reset();
String ha1str = colonJoiner.join(username,
authFields.get("realm"), password);
md5.update(ha1str.getBytes("ISO-8859-1"));
byte[] ha1bytes = md5.digest();
HA1 = bytesToHexString(ha1bytes);
}
catch(UnsupportedEncodingException e){
return null;
}
String HA2 = null;
try{
md5.reset();
String ha2str = colonJoiner.join(input.getRequestMethod(),
input.getURL().getPath());
md5.update(ha2str.getBytes("ISO-8859-1"));
HA2 = bytesToHexString(md5.digest());
}
catch(UnsupportedEncodingException e){
return null;
}
String HA3 = null;
try{
md5.reset();
String ha3str = colonJoiner.join(HA1, authFields.get("nonce"), HA2);
md5.update(ha3str.getBytes("ISO-8859-1"));
HA3 = bytesToHexString(md5.digest());
}
catch(UnsupportedEncodingException e){
return null;
}
StringBuilder sb = new StringBuilder(128);
sb.append("Digest ");
sb.append("username").append("=\"").append(username ).append("\",");
sb.append("realm" ).append("=\"").append(authFields.get("realm") ).append("\",");
sb.append("nonce" ).append("=\"").append(authFields.get("nonce") ).append("\",");
sb.append("uri" ).append("=\"").append(input.getURL().getPath()).append("\",");
//sb.append("qop" ).append('=' ).append("auth" ).append(",");
sb.append("response").append("=\"").append(HA3 ).append("\"");
try{
final HttpURLConnection result = (HttpURLConnection)input.getURL().openConnection();
result.addRequestProperty("Authorization", sb.toString());
return result;
}
catch(IOException e){
return null;
}
}
private static HashMap<String, String> splitAuthFields(String authString)
{
final HashMap<String, String> fields = Maps.newHashMap();
final CharMatcher trimmer = CharMatcher.anyOf("\"\t ");
final Splitter commas = Splitter.on(',').trimResults().omitEmptyStrings();
final Splitter equals = Splitter.on('=').trimResults(trimmer).limit(2);
String[] valuePair;
for(String keyPair : commas.split(authString)){
valuePair = Iterables.toArray(equals.split(keyPair), String.class);
fields.put(valuePair[0], valuePair[1]);
}
return fields;
}
private static final String HEX_LOOKUP = "0123456789abcdef";
private static String bytesToHexString(byte[] bytes)
{
StringBuilder sb = new StringBuilder(bytes.length * 2);
for(int i = 0; i < bytes.length; i++){
sb.append(HEX_LOOKUP.charAt((bytes[i] & 0xF0) >> 4));
sb.append(HEX_LOOKUP.charAt((bytes[i] & 0x0F) >> 0));
}
return sb.toString();
}
public static class AuthenticationException extends IOException
{
private static final long serialVersionUID = 1L;
public AuthenticationException()
{
super("Problems authenticating");
}
}
}
You can call the above code like this.
player = ExoPlayerFactory.newSimpleInstance(this.applicationContext)
exoPlayerView.player = player
exoPlayerView.useController = false
CoroutineScope(Dispatchers.IO).launch {
try {
val url = URL(item.url)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
val con = HttpDigestAuth.tryDigestAuthentication(
connection,
item.userName,
item.password
)
val request = con.getRequestProperty("Authorization")
CoroutineScope(Dispatchers.Main).launch {
try {
val uriString = item.url + "&resolution=640x480"
val uri = Uri.parse(uriString)
//ErrorController.showMessage("[BuildMediaSource] $uriString")
val mediaSource: MediaSource =
buildMediaSource(uri, request)
player?.playWhenReady = true
player?.prepare(mediaSource, true, false)
} catch (e: Exception) {
//ErrorController.showError(e)
}
}
} catch (e: Exception) {
//ErrorController.showError(e)
}
}
private fun buildMediaSource(uri: Uri, auth: String): MediaSource {
val userAgent = Util.getUserAgent(getContext(), packageName)
val factory = DefaultHttpDataSourceFactory(userAgent)
val property = factory.defaultRequestProperties
property.set("Authorization", auth)
val source = ProgressiveMediaSource.Factory(factory)
.createMediaSource(uri)
return source
}
You need the following dependency to make the code to work.
implementation 'com.google.guava:guava:30.1-android'
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.3'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.3'
'개발 일기' 카테고리의 다른 글
FindViewById 노가다 그만합시다. Prettify로 1초만에 findViewById 자동으로 다 하기. (0) | 2017.03.12 |
---|---|
EventBus로 여기저기서 쉽게 콜백 받기 (0) | 2016.12.28 |
Retrofit 2로 Json파싱 없이 Http통신 구현 시간을 단축해보자. (1) | 2016.11.07 |
selector auto creator (0) | 2016.10.18 |
Step 4. 음악 스트리밍 서버(Spring boot), 앱(Android)을 만들어보자. (6) | 2016.03.05 |