안 드 로 이 드 비디오 리퀘스트 의 실현 코드(방송 하면 서 캐 시)

약술 하 다
일부 유명한 동 영상 app 클 라 이언 트(유쿠,아이 치 이)가 동 영상 을 재생 할 때 캐 시 진도(2 급 진도 캐 시)가 있 고 짧 은 동 영상 app 도 있 으 며 모두 방송 하면 서 느 린 처 리 를 한다.그리고 파일 캐 시가 끝나 면 다시 재생 하면 네트워크 에 요청 하지 않 고 로 컬 파일 을 직접 재생 합 니 다.절 차 를 절약 할 뿐만 아니 라 로드 속도 도 높 였 다.
오늘 우 리 는 이 사 이 드 캐 시 를 실현 하 는 프레임 워 크 를 연구 토론 하 러 왔 다.왜냐하면 그것 은 어떠한 업무 논리 와 결합 되 지 않 기 때문이다.
오픈 소스 프로젝트
현재 비교적 좋 은 개원 프로젝트 는:https://github.com/danikula/AndroidVideoCache
코드 의 구조 도 잘 썼 습 니 다.네트워크 용 httpurlconnect,파일 캐 시 처리,파일 최대 전략,감청 처리,정지점 전송,프 록 시 서비스 등 이 있 습 니 다.연구 하고 읽 을 만하 다.
개인 적 으로 프로젝트 중 에 최적화 해 야 할 점 이 몇 가지 있다 고 생각 합 니 다.오늘 은 이 몇 가 지 를 처리 하고 원 리 를 간략하게 분석 하 겠 습 니 다.
최적화 점:
  • 파일 의 캐 시가 제한 을 초과 한 후 lru 알고리즘 에 따라 삭제 되 지 않 았 습 니 다.
  • 재생 기 에 되 돌아 오 는 http 응답 헤더 메 시 지 를 처리 하고 응답 헤더 메시지 의 가 져 오기 처 리 는 head 요청(서버 지원 필요)
  • 으로 변경 합 니 다.
  • 네트워크 라 이브 러 리 를 okhttp 로 교체 합 니 다.(대부분의 항목 이 okhttp 를 네트워크 요청 라 이브 러 리 로 하기 때 문 입 니 다)
  • 이 오픈 소스 프로젝트 의 원리 분석-로 컬 에이전트
  • 로 컬 프 록 시 서 비 스 를 사용 하여 원본 url 을 통 해 재생 기 에 로 컬 프 록 시 url 을 되 돌려 줍 니 다.프 록 시 URL 은 다음 과 같 습 니 다.http://127.0.0.1:57430/xxxx;그리고 재생 기 를 재생 할 때 로 컬 에이전트 에 요청 하 였 습 니 다.
  • 로 컬 프 록 시 는 ServerSocket 감청 127.0.0.1 의 유효한 포트 를 사용 합 니 다.이때 휴대 전 화 는 서버 입 니 다.클 라 이언 트 는 socket,즉 플레이어 입 니 다.
  • 읽 기 클 라 이언 트 는 socket 으로 데 이 터 를 읽 기(http 프로 토 콜 요청)http 프로 토 콜 을 분석 합 니 다.
  • url 에 따라 비디오 파일 이 존재 하 는 지 확인 하고 파일 데 이 터 를 읽 어 플레이어 에 보 내 는 것,즉 socket 에 데 이 터 를 기록 하 는 것 입 니 다.또한 다운로드 가 완료 되 지 않 으 면 정지점 에서 다운로드 합 니 다.물론 약 한 네트워크 의 경우 데 이 터 는 생산 소비 동기 화 처리 가 필요 합 니 다.
  • 최적화 점
    1.파일 의 캐 시가 제한 을 초과 한 후 lru 알고리즘 에 따라 삭제 되 지 않 았 습 니 다.
    Files 클래스.
    모 바 일 장치 에서 file.setLastModified()방법 은 밀리초 단위 의 시간 처 리 를 지원 하지 않 기 때문에 제한 크기 를 초과 한 후 오래된 것 을 삭제 해 야 하 는데 삭제 하지 않 고 이상 을 던 졌 습 니 다.주동 적 으로 던 진 이상 을 주석 하면 됩 니 다.파일 수정 시간 이 맞 으 니까.
    
      static void setLastModifiedNow(File file) throws IOException {
        if (file.exists()) {
          long now = System.currentTimeMillis();
          boolean modified = file.setLastModified(now/1000*1000); // on some devices (e.g. Nexus 5) doesn't work
          if (!modified) {
            modify(file);
    //        if (file.lastModified() < now) {
    //          VideoCacheLog.debug("LruDiskUsage", "modified not ok ");
    //          throw new IOException("Error set last modified date to " + file);
    //        }else{
    //          VideoCacheLog.debug("LruDiskUsage", "modified ok ");
    //        }
          }
        }
      }
    
    2.재생 기 에 되 돌아 오 는 http 응답 헤더 메 시 지 를 처리 하고 응답 헤더 메시지 의 가 져 오기 처 리 를 head 요청 으로 변경 합 니 다(서버 지원 이 필요 합 니 다)
    HttpUrlSource 류.fetch ContentInfo 방법 은 비디오 파일 의 Content-Type,Content-Length 정 보 를 가 져 오 는 것 입 니 다.재생 기 를 재생 할 때 재생 기 에 http 응답 헤드 정 보 를 조립 하기 위해 서 입 니 다.따라서 이 조각 은 데이터베이스 로 저장 해 야 합 니 다.재생 기 는 재생 할 때마다 여기 서 가 져 오지 마 십시오.요청 한 횟수 를 줄 이 고 데 이 터 를 절약 할 수 있 습 니 다.헤드 정보 만 필요 하고 응답 체 가 필요 없 기 때문에 우 리 는 얻 을 때 HEAD 방법 을 직접 사용 할 수 있 습 니 다.그래서 코드 에 openConnection ForHeader 를 추가 하 는 방법 은 다음 과 같 습 니 다.
    
     private void fetchContentInfo() throws ProxyCacheException {
        VideoCacheLog.debug(TAG,"Read content info from " + sourceInfo.url);
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        try {
          urlConnection = openConnectionForHeader(20000);
          long length = getContentLength(urlConnection);
          String mime = urlConnection.getContentType();
          inputStream = urlConnection.getInputStream();
          this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
          this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
          VideoCacheLog.debug(TAG,"Source info fetched: " + sourceInfo);
        } catch (IOException e) {
          VideoCacheLog.error(TAG,"Error fetching info from " + sourceInfo.url ,e);
        } finally {
          ProxyCacheUtils.close(inputStream);
          if (urlConnection != null) {
            urlConnection.disconnect();
          }
        }
      }
    
      // for HEAD
      private HttpURLConnection openConnectionForHeader(int timeout) throws IOException, ProxyCacheException {
        HttpURLConnection connection;
        boolean redirected;
        int redirectCount = 0;
        String url = this.sourceInfo.url;
        do {
          VideoCacheLog.debug(TAG, "Open connection for header to " + url);
          connection = (HttpURLConnection) new URL(url).openConnection();
          if (timeout > 0) {
            connection.setConnectTimeout(timeout);
            connection.setReadTimeout(timeout);
          }
          //     ,   BODY,                  
          connection.setRequestMethod("HEAD");
          int code = connection.getResponseCode();
          redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
          if (redirected) {
            url = connection.getHeaderField("Location");
            VideoCacheLog.debug(TAG,"Redirect to:" + url);
            redirectCount++;
            connection.disconnect();
            VideoCacheLog.debug(TAG,"Redirect closed:" + url);
          }
          if (redirectCount > MAX_REDIRECTS) {
            throw new ProxyCacheException("Too many redirects: " + redirectCount);
          }
        } while (redirected);
        return connection;
      }
    
    
    3.네트워크 라 이브 러 리 를 okhttp 로 교체 합 니 다(대부분의 항목 은 okhttp 를 네트워크 요청 라 이브 러 리 로 하기 때 문 입 니 다)
    왜 우리 가 바 꿔 야 되 지?!첫째,OKHttp 는 효율 적 인 HTTP 클 라 이언 트 로 같은 주 소 를 연결 하 는 링크 가 같은 socket 을 공유 하 는 것 을 지원 합 니 다.연결 풀 을 통 해 응답 지연 을 줄 이 고 투명 한 GZIP 압축,캐 시 요청 등 장점 도 있 습 니 다.그 핵심 은 주로 경로,연결 프로 토 콜,차단기,대리,안전성 인증,연결 풀 과 네트워크 어댑터 가 있 습 니 다.차단 기 는 주로 추 가 를 말 합 니 다.요청 이나 응답 하 는 머리 정 보 를 제거 하거나 변환 합 니 다.안 드 로 이 드 개발 의 인정 을 받 았 습 니 다.둘째,대부분의 app 은 OKHttp 를 사용 하고 구 글 은 이 를 안 드 로 이 드 소스 코드 에 포함 시 킵 니 다.셋째,이 작성 자 코드 에 사 용 된 httpurlconnet 은 HttpUrlSource 에서 다음 과 같은 부분 이 있 습 니 다.
    
     @Override
      public void close() throws ProxyCacheException {
        if (connection != null) {
          try {
            connection.disconnect();
          } catch (NullPointerException | IllegalArgumentException e) {
            String message = "Wait... but why? WTF!? " +
                "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
                "If you read it on your device log, please, notify me [email protected] or create issue here " +
                "https://github.com/danikula/AndroidVideoCache/issues.";
            throw new RuntimeException(message, e);
          } catch (ArrayIndexOutOfBoundsException e) {
            VideoCacheLog.error(TAG,"Error closing connection correctly. Should happen only on Android L. " +
                "If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
                "Until good solution is not know, just ignore this issue :(", e);
          }
        }
      }
    
    okhttp 와 같은 우수한 네트워크 오픈 소스 프로젝트 가 없 기 전에 안 드 로 이 드 개발 은 httpurlconnet 이나 httpclient 를 사용 합 니 다.일부 휴대 전 화 는 이 문 제 를 만 날 수 있 습 니 다.
    여기 서 사용 하 는 copile'com.squareup.okhttp:okhttp:2.7.5'버 전 으로 이러한 기능 을 실현 합 니 다.원작 자의 구조 사고방식 에서 우 리 는 Source 인 터 페 이 스 를 실현 하 는 종류 인 OkHttpUrlSource 만 늘 리 면 된다.이 를 통 해 작가 의 코드 구조 가 괜찮다 는 것 을 알 수 있다.물론 우 리 는 앞에서 향상 시 킨 최적화 점 2 중의 문 제 를 처리 해 야 한다.프로젝트 에 HttpUrlSource 를 사용 하 는 모든 곳 을 OkHttpUrlSource 로 바 꾸 면 됩 니 다.
    원본 코드 는 다음 과 같 습 니 다.
    
    /**
     * ================================================
     *     :   
    
     *     :
     *     :2017/4/13-  12:03
     *     :   Android   HttpURLConnection.disconnect()        ,
     *     MediaPlayer           ,      okhttp  HttpURLConnection
     */
    
    public class OkHttpUrlSource implements Source {
    
      private static final String TAG = OkHttpUrlSource.class.getSimpleName();
      private static final int MAX_REDIRECTS = 5;
      private final SourceInfoStorage sourceInfoStorage;
      private SourceInfo sourceInfo;
      private OkHttpClient okHttpClient = new OkHttpClient();
      private Call requestCall = null;
      private InputStream inputStream;
    
      public OkHttpUrlSource(String url) {
        this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
      }
    
      public OkHttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
        this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
        SourceInfo sourceInfo = sourceInfoStorage.get(url);
        this.sourceInfo = sourceInfo != null ? sourceInfo :
            new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
      }
    
      public OkHttpUrlSource(OkHttpUrlSource source) {
        this.sourceInfo = source.sourceInfo;
        this.sourceInfoStorage = source.sourceInfoStorage;
      }
    
      @Override
      public synchronized long length() throws ProxyCacheException {
        if (sourceInfo.length == Integer.MIN_VALUE) {
          fetchContentInfo();
        }
        return sourceInfo.length;
      }
    
      @Override
      public void open(long offset) throws ProxyCacheException {
        try {
          Response response = openConnection(offset, -1);
          String mime = response.header("Content-Type");
          this.inputStream = new BufferedInputStream(response.body().byteStream(), DEFAULT_BUFFER_SIZE);
          long length = readSourceAvailableBytes(response, offset, response.code());
          this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
          this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
        } catch (IOException e) {
          throw new ProxyCacheException("Error opening okHttpClient for " + sourceInfo.url + " with offset " + offset, e);
        }
      }
    
      private long readSourceAvailableBytes(Response response, long offset, int responseCode) throws IOException {
        long contentLength = getContentLength(response);
        return responseCode == HTTP_OK ? contentLength
            : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
      }
    
      private long getContentLength(Response response) {
        String contentLengthValue = response.header("Content-Length");
        return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
      }
    
      @Override
      public void close() throws ProxyCacheException {
        if (okHttpClient != null && inputStream != null && requestCall != null) {
          try {
            inputStream.close();
            requestCall.cancel();
          } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage(), e);
          }
        }
      }
    
      @Override
      public int read(byte[] buffer) throws ProxyCacheException {
        if (inputStream == null) {
          throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": okHttpClient is absent!");
        }
        try {
          return inputStream.read(buffer, 0, buffer.length);
        } catch (InterruptedIOException e) {
          throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
        } catch (IOException e) {
          throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
        }
      }
    
      private void fetchContentInfo() throws ProxyCacheException {
        VideoCacheLog.debug(TAG, "Read content info from " + sourceInfo.url);
        Response response = null;
        InputStream inputStream = null;
        try {
          response = openConnectionForHeader(20000);
          if (response == null || !response.isSuccessful()) {
            throw new ProxyCacheException("Fail to fetchContentInfo: " + sourceInfo.url);
          }
          long length = getContentLength(response);
          String mime = response.header("Content-Type", "application/mp4");
          inputStream = response.body().byteStream();
          this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
          this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
          VideoCacheLog.info(TAG, "Content info for `" + sourceInfo.url + "`: mime: " + mime + ", content-length: " + length);
        } catch (IOException e) {
          VideoCacheLog.error(TAG, "Error fetching info from " + sourceInfo.url, e);
        } finally {
          ProxyCacheUtils.close(inputStream);
          if (response != null && requestCall != null) {
            requestCall.cancel();
          }
        }
      }
    
      // for HEAD
      private Response openConnectionForHeader(int timeout) throws IOException, ProxyCacheException {
        if (timeout > 0) {
    //      okHttpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);
    //      okHttpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS);
    //      okHttpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS);
        }
        Response response;
        boolean isRedirect = false;
        String newUrl = this.sourceInfo.url;
        int redirectCount = 0;
        do {
          //     ,   BODY,                  
          Request request = new Request.Builder()
              .head()
              .url(newUrl)
              .build();
          requestCall = okHttpClient.newCall(request);
          response = requestCall.execute();
          if (response.isRedirect()) {
            newUrl = response.header("Location");
            VideoCacheLog.debug(TAG, "Redirect to:" + newUrl);
            isRedirect = response.isRedirect();
            redirectCount++;
            requestCall.cancel();
            VideoCacheLog.debug(TAG, "Redirect closed:" + newUrl);
          }
          if (redirectCount > MAX_REDIRECTS) {
            throw new ProxyCacheException("Too many redirects: " + redirectCount);
          }
        } while (isRedirect);
    
        return response;
      }
    
      private Response openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
        if (timeout > 0) {
    //      okHttpClient.setConnectTimeout(timeout, TimeUnit.MILLISECONDS);
    //      okHttpClient.setReadTimeout(timeout, TimeUnit.MILLISECONDS);
    //      okHttpClient.setWriteTimeout(timeout, TimeUnit.MILLISECONDS);
        }
        Response response;
        boolean isRedirect = false;
        String newUrl = this.sourceInfo.url;
        int redirectCount = 0;
        do {
          VideoCacheLog.debug(TAG, "Open connection" + (offset > 0 ? " with offset " + offset : "") + " to " + sourceInfo.url);
          Request.Builder requestBuilder = new Request.Builder()
              .get()
              .url(newUrl);
          if (offset > 0) {
            requestBuilder.addHeader("Range", "bytes=" + offset + "-");
          }
          requestCall = okHttpClient.newCall(requestBuilder.build());
          response = requestCall.execute();
          if (response.isRedirect()) {
            newUrl = response.header("Location");
            isRedirect = response.isRedirect();
            redirectCount++;
          }
          if (redirectCount > MAX_REDIRECTS) {
            throw new ProxyCacheException("Too many redirects: " + redirectCount);
          }
        } while (isRedirect);
    
        return response;
      }
    
      public synchronized String getMime() throws ProxyCacheException {
        if (TextUtils.isEmpty(sourceInfo.mime)) {
          fetchContentInfo();
        }
        return sourceInfo.mime;
      }
    
      public String getUrl() {
        return sourceInfo.url;
      }
    
      @Override
      public String toString() {
        return "OkHttpUrlSource{sourceInfo='" + sourceInfo + "}";
      }
    }
    
    
    이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

    좋은 웹페이지 즐겨찾기