text:
package com.miz.mizuu.fragments;

import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.text.Html;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import com.melnykov.fab.FloatingActionButton;
import com.miz.apis.trakt.Trakt;
import com.miz.db.DbAdapterTvShowEpisodes;
import com.miz.functions.BlurTransformation;
import com.miz.functions.FileSource;
import com.miz.functions.Filepath;
import com.miz.functions.MizLib;
import com.miz.functions.PaletteLoader;
import com.miz.functions.SimpleAnimatorListener;
import com.miz.mizuu.EditTvShowEpisode;
import com.miz.mizuu.IdentifyTvShowEpisode;
import com.miz.mizuu.Main;
import com.miz.mizuu.MizuuApplication;
import com.miz.mizuu.R;
import com.miz.mizuu.TvShowEpisode;
import com.miz.service.DeleteFile;
import com.miz.utils.LocalBroadcastUtils;
import com.miz.utils.TvShowDatabaseUtils;
import com.miz.utils.TypefaceUtils;
import com.miz.utils.VideoUtils;
import com.miz.utils.ViewUtils;
import com.miz.views.ObservableScrollView;
import com.miz.views.ObservableScrollView.OnScrollChangedListener;
import com.squareup.otto.Bus;
import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;

import java.util.ArrayList;

import static com.miz.functions.PreferenceKeys.ALWAYS_DELETE_FILE;
import static com.miz.functions.PreferenceKeys.SHOW_FILE_LOCATION;

@SuppressLint("InflateParams") public class TvShowEpisodeDetailsFragment extends Fragment {

    private Activity mContext;
    private TvShowEpisode mEpisode;
    private ImageView mBackdrop, mEpisodePhoto;
    private TextView mTitle, mDescription, mFileSource, mAirDate, mRating, mDirector, mWriter, mGuestStars, mSeasonEpisodeNumber;
    private View mDetailsArea;
    private Picasso mPicasso;
    private Typeface mMediumItalic, mMedium, mCondensedRegular;
    private DbAdapterTvShowEpisodes mDatabaseHelper;
    private long mVideoPlaybackStarted, mVideoPlaybackEnded;
    private boolean mShowFileLocation;
    private Bus mBus;
    private int mToolbarColor = 0;
    private FloatingActionButton mFab;
    private PaletteLoader mPaletteLoader;
    private ObservableScrollView mScrollView;

    /**
     * Empty constructor as per the Fragment documentation
     */
    public TvShowEpisodeDetailsFragment() {}

    public static TvShowEpisodeDetailsFragment newInstance(String showId, int season, int episode) {
        TvShowEpisodeDetailsFragment pageFragment = new TvShowEpisodeDetailsFragment();
        Bundle bundle = new Bundle();
        bundle.putString("showId", showId);
        bundle.putInt("season", season);
        bundle.putInt("episode", episode);
        pageFragment.setArguments(bundle);
        return pageFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setRetainInstance(true);
        setHasOptionsMenu(true);

        mContext = getActivity();

        mBus = MizuuApplication.getBus();

        mShowFileLocation = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(SHOW_FILE_LOCATION, true);

        mPicasso = MizuuApplication.getPicasso(getActivity());

        mMediumItalic = TypefaceUtils.getRobotoMediumItalic(mContext);
        mMedium = TypefaceUtils.getRobotoMedium(mContext);
        mCondensedRegular = TypefaceUtils.getRobotoCondensedRegular(mContext);

        mDatabaseHelper = MizuuApplication.getTvEpisodeDbAdapter();

        LocalBroadcastManager.getInstance(mContext).registerReceiver(mBroadcastReceiver,
                new IntentFilter(LocalBroadcastUtils.UPDATE_TV_SHOW_EPISODE_DETAILS_OVERVIEW));

        loadEpisode();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mBroadcastReceiver);
    }

    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            loadEpisode();
            loadData();
        }
    };

    private void loadEpisode() {
        if (!getArguments().getString("showId").isEmpty() && getArguments().getInt("season") >= 0 && getArguments().getInt("episode") >= 0) {
            Cursor cursor = mDatabaseHelper.getEpisode(getArguments().getString("showId"), getArguments().getInt("season"), getArguments().getInt("episode"));

            if (cursor.moveToFirst()) {
                mEpisode = new TvShowEpisode(getActivity(),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_SHOW_ID)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_TITLE)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_PLOT)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_SEASON)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_AIRDATE)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_DIRECTOR)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_WRITER)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_GUESTSTARS)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE_RATING)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_HAS_WATCHED)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_FAVOURITE))
                );

                mEpisode.setFilepaths(MizuuApplication.getTvShowEpisodeMappingsDbAdapter().getFilepathsForEpisode(
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_SHOW_ID)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_SEASON)),
                        cursor.getString(cursor.getColumnIndex(DbAdapterTvShowEpisodes.KEY_EPISODE))
                ));
            }
            cursor.close();
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.episode_details, container, false);
    }

    @Override
    public void onViewCreated(final View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mBackdrop = (ImageView) view.findViewById(R.id.imageBackground);
        mEpisodePhoto = (ImageView) view.findViewById(R.id.episodePhoto);
        mDetailsArea = view.findViewById(R.id.details_area);

        mTitle = (TextView) view.findViewById(R.id.movieTitle);
        mSeasonEpisodeNumber = (TextView) view.findViewById(R.id.textView7);
        mDescription = (TextView) view.findViewById(R.id.textView2);
        mFileSource = (TextView) view.findViewById(R.id.textView3);
        mAirDate = (TextView) view.findViewById(R.id.textReleaseDate);
        mRating = (TextView) view.findViewById(R.id.textView12);
        mDirector = (TextView) view.findViewById(R.id.director);
        mWriter = (TextView) view.findViewById(R.id.writer);
        mGuestStars = (TextView) view.findViewById(R.id.guest_stars);
        mScrollView = (ObservableScrollView) view.findViewById(R.id.observableScrollView);
        mFab = (FloatingActionButton) view.findViewById(R.id.fab);

        mFab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewUtils.animateFabJump(v, new SimpleAnimatorListener() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        play();
                    }
                });
            }
        });
        if (MizLib.isTablet(mContext))
            mFab.setType(FloatingActionButton.TYPE_NORMAL);

        final int height = MizLib.getActionBarAndStatusBarHeight(getActivity());

        mScrollView = (ObservableScrollView) view.findViewById(R.id.observableScrollView);
        mScrollView.setOnScrollChangedListener(new OnScrollChangedListener() {
            @Override
            public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
                final int headerHeight = mEpisodePhoto.getHeight() - height;
                final float ratio = (float) Math.min(Math.max(t, 0), headerHeight) / headerHeight;
                final int newAlpha = (int) (ratio * 255);

                mBus.post(new BusToolbarColorObject(mToolbarColor, newAlpha));

                if (MizLib.isPortrait(mContext)) {
                    // Such parallax, much wow
                    mEpisodePhoto.setPadding(0, (int) (t / 1.5), 0, 0);
                }
            }
        });
        mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ViewUtils.setLayoutParamsForDetailsEmptyView(mContext, view,
                        mBackdrop, mScrollView, this);
            }
        });

        loadData();

        mPicasso.load(mEpisode.getEpisodePhoto()).placeholder(R.drawable.bg).config(MizuuApplication.getBitmapConfig()).into(mEpisodePhoto, new Callback() {
            @Override
            public void onError() {
                if (!isAdded())
                    return;
                int width = getActivity().getResources().getDimensionPixelSize(R.dimen.episode_details_background_overlay_width);
                int height = getActivity().getResources().getDimensionPixelSize(R.dimen.episode_details_background_overlay_height);
                mPicasso.load(mEpisode.getTvShowBackdrop()).placeholder(R.drawable.bg).error(R.drawable.nobackdrop).resize(width, height).config(MizuuApplication.getBitmapConfig()).into(mEpisodePhoto);
            }

            @Override
            public void onSuccess() {
                if (mPaletteLoader == null) {
                    mPaletteLoader = new PaletteLoader(mPicasso, Uri.fromFile(mEpisode.getEpisodePhoto()), new PaletteLoader.OnPaletteLoadedCallback() {
                        @Override
                        public void onPaletteLoaded(int swatchColor) {
                            mToolbarColor = swatchColor;
                        }
                    });

                    mPaletteLoader.addView(mDetailsArea);
                    mPaletteLoader.setFab(mFab);

                    mPaletteLoader.execute();
                } else {
                    // Clear old views after configuration change
                    mPaletteLoader.clearViews();

                    // Add views after configuration change
                    mPaletteLoader.addView(mDetailsArea);
                    mPaletteLoader.setFab(mFab);

                    // Re-color the views
                    mPaletteLoader.colorViews();
                }
            }
        });

        if (!MizLib.isPortrait(getActivity()))
            mPicasso.load(mEpisode.getEpisodePhoto()).placeholder(R.drawable.bg).error(R.drawable.bg).transform(new BlurTransformation(getActivity().getApplicationContext(), mEpisode.getEpisodePhoto().getAbsolutePath() + "-blur", 4)).into(mBackdrop, new Callback() {
                @Override public void onError() {
                    if (!isAdded())
                        return;

                    mPicasso.load(mEpisode.getTvShowBackdrop()).placeholder(R.drawable.bg).error(R.drawable.nobackdrop).transform(new BlurTransformation(getActivity().getApplicationContext(), mEpisode.getTvShowBackdrop().getAbsolutePath() + "-blur", 4)).into(mBackdrop, new Callback() {
                        @Override
                        public void onError() {}

                        @Override
                        public void onSuccess() {
                            if (!isAdded())
                                return;
                            mBackdrop.setColorFilter(Color.parseColor("#aa181818"), android.graphics.PorterDuff.Mode.SRC_OVER);
                        }
                    });
                }

                @Override
                public void onSuccess() {
                    if (!isAdded())
                        return;
                    mBackdrop.setColorFilter(Color.parseColor("#aa181818"), android.graphics.PorterDuff.Mode.SRC_OVER);
                }
            });
    }

    private void loadData() {
        // Set the episode title
        mTitle.setVisibility(View.VISIBLE);
        mTitle.setText(mEpisode.getTitle());
        mTitle.setTypeface(mCondensedRegular);

        mDescription.setTypeface(mCondensedRegular);
        mFileSource.setTypeface(mCondensedRegular);
        mDirector.setTypeface(mCondensedRegular);
        mWriter.setTypeface(mCondensedRegular);
        mGuestStars.setTypeface(mCondensedRegular);

        mAirDate.setTypeface(mMedium);
        mRating.setTypeface(mMedium);
        mSeasonEpisodeNumber.setTypeface(mMediumItalic);
        mSeasonEpisodeNumber.setText(getString(R.string.showSeason) + " " + mEpisode.getSeason() + ", " + getString(R.string.showEpisode) + " " + mEpisode.getEpisode());

        // Set the movie plot
        if (!MizLib.isPortrait(getActivity())) {
            mDescription.setBackgroundResource(R.drawable.selectable_background);
            mDescription.setMaxLines(getActivity().getResources().getInteger(R.integer.episode_details_max_lines));
            mDescription.setTag(true); // true = collapsed
            mDescription.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (((Boolean) mDescription.getTag())) {
                        mDescription.setMaxLines(1000);
                        mDescription.setTag(false);
                    } else {
                        mDescription.setMaxLines(getActivity().getResources().getInteger(R.integer.episode_details_max_lines));
                        mDescription.setTag(true);
                    }
                }
            });
            mDescription.setEllipsize(TextUtils.TruncateAt.END);
            mDescription.setFocusable(true);
        } else {
            if (MizLib.isTablet(getActivity()))
                mDescription.setLineSpacing(0, 1.15f);
        }
        mDescription.setText(mEpisode.getDescription());

        if (mShowFileLocation) {
            mFileSource.setText(mEpisode.getAllFilepaths());
        } else {
            mFileSource.setVisibility(View.GONE);
        }

        // Set the episode air date
        mAirDate.setText(MizLib.getPrettyDatePrecise(getActivity(), mEpisode.getReleasedate()));

        // Set the movie rating
        if (!mEpisode.getRating().equals("0.0")) {
            try {
                int rating = (int) (Double.parseDouble(mEpisode.getRating()) * 10);
                mRating.setText(Html.fromHtml(rating + "<small> %</small>"));
            } catch (NumberFormatException e) {
                mRating.setText(mEpisode.getRating());
            }
        } else {
            mRating.setText(R.string.stringNA);
        }

        if (TextUtils.isEmpty(mEpisode.getDirector()) || mEpisode.getDirector().equals(getString(R.string.stringNA))) {
            mDirector.setVisibility(View.GONE);
        } else {
            mDirector.setText(mEpisode.getDirector());
        }

        if (TextUtils.isEmpty(mEpisode.getWriter()) || mEpisode.getWriter().equals(getString(R.string.stringNA))) {
            mWriter.setVisibility(View.GONE);
        } else {
            mWriter.setText(mEpisode.getWriter());
        }

        if (TextUtils.isEmpty(mEpisode.getGuestStars()) || mEpisode.getGuestStars().equals(getString(R.string.stringNA))) {
            mGuestStars.setVisibility(View.GONE);
        } else {
            mGuestStars.setText(mEpisode.getGuestStars());
        }
    }

    private void play() {
        ArrayList<Filepath> paths = mEpisode.getFilepaths();
        if (paths.size() == 1) {
            Filepath path = paths.get(0);
            if (mEpisode.hasOfflineCopy(path)) {
                boolean playbackStarted = VideoUtils.playVideo(getActivity(), mEpisode.getOfflineCopyUri(path), FileSource.FILE, mEpisode);
                if (playbackStarted) {
                    mVideoPlaybackStarted = System.currentTimeMillis();
                    checkIn();
                }
            } else {
                boolean playbackStarted = VideoUtils.playVideo(getActivity(), path.getFilepath(), path.getType(), mEpisode);
                if (playbackStarted) {
                    mVideoPlaybackStarted = System.currentTimeMillis();
                    checkIn();
                }
            }
        } else {
            boolean hasOfflineCopy = false;
            for (Filepath path : paths) {
                if (mEpisode.hasOfflineCopy(path)) {
                    boolean playbackStarted = VideoUtils.playVideo(getActivity(), mEpisode.getOfflineCopyUri(path), FileSource.FILE, mEpisode);
                    if (playbackStarted) {
                        mVideoPlaybackStarted = System.currentTimeMillis();
                        checkIn();
                    }

                    hasOfflineCopy = true;
                    break;
                }
            }

            if (!hasOfflineCopy) {
                MizLib.showSelectFileDialog(getActivity(), mEpisode.getFilepaths(), new Dialog.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Filepath path = mEpisode.getFilepaths().get(which);
                        boolean playbackStarted = VideoUtils.playVideo(getActivity(), path.getFilepath(), path.getType(), mEpisode);
                        if (playbackStarted) {
                            mVideoPlaybackStarted = System.currentTimeMillis();
                            checkIn();
                        }
                    }
                });
            }
        }
    }

    public void onResume() {
        super.onResume();

        mBus.register(getActivity());

        mVideoPlaybackEnded = System.currentTimeMillis();

        if (mVideoPlaybackStarted > 0 && mVideoPlaybackEnded - mVideoPlaybackStarted > (1000 * 60 * 5)) {
            if (!mEpisode.hasWatched())
                watched(false); // Mark it as watched
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.episode_details, menu);

        try {
            if (mEpisode.hasWatched()) {
                menu.findItem(R.id.watched).setTitle(R.string.stringMarkAsUnwatched);
            } else {
                menu.findItem(R.id.watched).setTitle(R.string.stringMarkAsWatched);
            }
        } catch (Exception e) {}
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menuDeleteEpisode:
                deleteEpisode();
                break;
            case R.id.watched:
                watched(true);
                break;
            case R.id.identify:
                identifyEpisode();
                break;
            case R.id.editTvShowEpisode:
                editEpisode();
                break;
        }
        return false;
    }

    private void editEpisode() {
        Intent intent = new Intent(getActivity(), EditTvShowEpisode.class);
        intent.putExtra("showId", mEpisode.getShowId());
        intent.putExtra("season", MizLib.getInteger(mEpisode.getSeason()));
        intent.putExtra("episode", MizLib.getInteger(mEpisode.getEpisode()));
        startActivityForResult(intent, 0);
    }

    private void identifyEpisode() {
        if (mEpisode.getFilepaths().size() == 1) {
            getActivity().startActivityForResult(getIdentifyIntent(mEpisode.getFilepaths().get(0).getFullFilepath()), 0);

        } else {
            MizLib.showSelectFileDialog(getActivity(), mEpisode.getFilepaths(), new Dialog.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    getActivity().startActivityForResult(getIdentifyIntent(mEpisode.getFilepaths().get(which).getFullFilepath()), 0);

                    // Dismiss the dialog
                    dialog.dismiss();
                }
            });
        }
    }

    private Intent getIdentifyIntent(String filepath) {
        Intent i = new Intent(getActivity(), IdentifyTvShowEpisode.class);
        ArrayList<String> filepaths = new ArrayList<String>();
        filepaths.add(filepath);
        i.putExtra("filepaths", filepaths);
        i.putExtra("showId", mEpisode.getShowId());
        i.putExtra("showTitle", MizuuApplication.getTvDbAdapter().getShowTitle(mEpisode.getShowId()));
        return i;
    }

    private void deleteEpisode() {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

        View dialogLayout = getActivity().getLayoutInflater().inflate(R.layout.delete_file_dialog_layout, null);
        final CheckBox cb = (CheckBox) dialogLayout.findViewById(R.id.deleteFile);
        cb.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(ALWAYS_DELETE_FILE, true));

        builder.setTitle(getString(R.string.removeEpisode) + " S" + mEpisode.getSeason() + "E" + mEpisode.getEpisode())
                .setView(dialogLayout)
                .setCancelable(false)
                .setPositiveButton(getString(android.R.string.yes), new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        TvShowDatabaseUtils.deleteEpisode(mContext, mEpisode.getShowId(),
                                MizLib.getInteger(mEpisode.getSeason()), MizLib.getInteger(mEpisode.getEpisode()));

                        if (cb.isChecked()) {
                            for (Filepath path : mEpisode.getFilepaths()) {
                                Intent deleteIntent = new Intent(getActivity(), DeleteFile.class);
                                deleteIntent.putExtra("filepath", path.getFilepath());
                                getActivity().startService(deleteIntent);
                            }
                        }

                        if (MizuuApplication.getTvEpisodeDbAdapter().getEpisodeCount(mEpisode.getShowId()) == 0) {
                            // The show has been deleted! Let's show the TV show library overview

                            Intent i = new Intent(mContext, Main.class);
                            i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                            i.putExtra("startup", String.valueOf(Main.SHOWS));
                            startActivity(i);
                        } else {
                            LocalBroadcastUtils.updateTvShowSeasonsOverview(mContext);
                            LocalBroadcastUtils.updateTvShowEpisodesOverview(mContext);
                        }

                        notifyDatasetChanges();
                        getActivity().finish();
                    }
                })
                .setNegativeButton(getString(android.R.string.no), new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.cancel();
                    }
                })
                .show();
    }

    private void watched(boolean showToast) {
        // Create and open database
        mDatabaseHelper = MizuuApplication.getTvEpisodeDbAdapter();

        mEpisode.setHasWatched(!mEpisode.hasWatched()); // Reverse the hasWatched boolean

        if (mDatabaseHelper.setEpisodeWatchStatus(mEpisode.getShowId(), mEpisode.getSeason(), mEpisode.getEpisode(), mEpisode.hasWatched())) {
            getActivity().invalidateOptionsMenu();

            if (showToast)
                if (mEpisode.hasWatched()) {
                    Toast.makeText(getActivity(), getString(R.string.markedAsWatched), Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(getActivity(), getString(R.string.markedAsUnwatched), Toast.LENGTH_SHORT).show();
                }
        } else {
            if (showToast)
                Toast.makeText(getActivity(), getString(R.string.errorOccured), Toast.LENGTH_SHORT).show();
        }

        mBus.post(mEpisode);

        new Thread() {
            @Override
            public void run() {
                ArrayList<com.miz.functions.TvShowEpisode> episode = new ArrayList<com.miz.functions.TvShowEpisode>();
                episode.add(new com.miz.functions.TvShowEpisode(mEpisode.getShowId(), Integer.valueOf(mEpisode.getEpisode()), Integer.valueOf(mEpisode.getSeason())));
                Trakt.markEpisodeAsWatched(mEpisode.getShowId(), episode, getActivity(), false);
            }
        }.start();
    }

    private void notifyDatasetChanges() {
        LocalBroadcastUtils.updateTvShowLibrary(getActivity());
    }

    private void checkIn() {
        new Thread() {
            @Override
            public void run() {
                Trakt.performEpisodeCheckin(mEpisode, getActivity());
            }
        }.start();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 0) {
            if (resultCode == Activity.RESULT_OK) {
                loadEpisode();
                loadData();
            }
        }
    }

    public class BusToolbarColorObject {

        private final int mToolbarColor, mAlpha;

        public BusToolbarColorObject(int toolbarColor, int alpha) {
            mToolbarColor = toolbarColor;
            mAlpha = alpha;
        }

        public int getToolbarColor() {
            return mToolbarColor;
        }

        public int getAlpha() {
            return mAlpha;
        }
    }
}


text:
package org.gmarz.googleplaces.query;

public abstract class Query {

	protected QueryBuilder mQueryBuilder = new QueryBuilder();

	public Query() {
		setSensor(false); // Default
	}

	public void setSensor(boolean sensor) {
		mQueryBuilder.addParameter("sensor", Boolean.toString(sensor));
	}

	public void setLanguage(String language) {
		mQueryBuilder.addParameter("language", language);
	}

	public abstract String getUrl();

	@Override
	public String toString() {
		return (getUrl() + mQueryBuilder.toString());
	}
}

redpajama_set_name: RedPajamaGithub


text:
namespace ash::device_activity {

namespace psm_rlwe = private_membership::rlwe;

DailyUseCaseImpl::DailyUseCaseImpl(
    const std::string& psm_device_active_secret,
    const ChromeDeviceMetadataParameters& chrome_passed_device_params,
    PrefService* local_state,
    std::unique_ptr<PsmDelegateInterface> psm_delegate)
    : DeviceActiveUseCase(psm_device_active_secret,
                          chrome_passed_device_params,
                          prefs::kDeviceActiveLastKnownDailyPingTimestamp,
                          psm_rlwe::RlweUseCase::CROS_FRESNEL_DAILY,
                          local_state,
                          std::move(psm_delegate)) {}

DailyUseCaseImpl::~DailyUseCaseImpl() = default;

std::string DailyUseCaseImpl::GenerateUTCWindowIdentifier(base::Time ts) const {
  base::Time::Exploded exploded;
  ts.UTCExplode(&exploded);
  return base::StringPrintf("%04d%02d%02d", exploded.year, exploded.month,
                            exploded.day_of_month);
}

ImportDataRequest DailyUseCaseImpl::GenerateImportRequestBody() {
  std::string psm_id_str = GetPsmIdentifier().value().sensitive_id();
  std::string window_id_str = GetWindowIdentifier().value();

  // Generate Fresnel PSM import request body.
  device_activity::ImportDataRequest import_request;
  import_request.set_window_identifier(window_id_str);

  // Create fresh |DeviceMetadata| object.
  // Note every dimension added to this proto must be approved by privacy.
  DeviceMetadata* device_metadata = import_request.mutable_device_metadata();
  device_metadata->set_chromeos_version(GetChromeOSVersion());
  device_metadata->set_chromeos_channel(GetChromeOSChannel());
  device_metadata->set_market_segment(GetMarketSegment());

  // TODO(hirthanan): This is used for debugging purposes until crbug/1289722
  // has launched.
  device_metadata->set_hardware_id(GetFullHardwareClass());

  import_request.set_use_case(GetPsmUseCase());
  import_request.set_plaintext_identifier(psm_id_str);

  return import_request;
}

}  // namespace ash::device_activity


text:
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.cpe4235.chapter4.data;

import java.util.Date;
import java.util.Objects;

/**
 *
 * @author BANK.CPE
 */
public class Patient {

    private String hcode;
    private String pid;
    private String drugName;
    private Date visitDate;
    private float qty;
    private String unit;

    public String getHcode() {
        return hcode;
    }

    public void setHcode(String hcode) {
        this.hcode = hcode;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public String getDrugName() {
        return drugName;
    }

    public void setDrugName(String drugName) {
        this.drugName = drugName;
    }

    public Date getVisitDate() {
        return visitDate;
    }

    public void setVisitDate(Date visitDate) {
        this.visitDate = visitDate;
    }

    public float getQty() {
        return qty;
    }

    public void setQty(float qty) {
        this.qty = qty;
    }

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 67 * hash + Objects.hashCode(this.hcode);
        hash = 67 * hash + Objects.hashCode(this.pid);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Patient other = (Patient) obj;
        if (!Objects.equals(this.hcode, other.hcode)) {
            return false;
        }
        if (!Objects.equals(this.pid, other.pid)) {
            return false;
        }
        return true;
    }

}


text:
namespace media {

static base::Time ExtractTimelineOffset(AVFormatContext* format_context) {
  if (strstr(format_context->iformat->name, "webm") ||
      strstr(format_context->iformat->name, "matroska")) {
    const AVDictionaryEntry* entry =
        av_dict_get(format_context->metadata, "creation_time", NULL, 0);

    base::Time timeline_offset;
    if (entry != NULL && entry->value != NULL &&
        FFmpegUTCDateToTime(entry->value, &timeline_offset)) {
      return timeline_offset;
    }
  }

  return base::Time();
}

static base::TimeDelta FramesToTimeDelta(int frames, double sample_rate) {
  return base::TimeDelta::FromMicroseconds(
      frames * base::Time::kMicrosecondsPerSecond / sample_rate);
}

static base::TimeDelta ExtractStartTime(AVStream* stream,
                                        base::TimeDelta start_time_estimate) {
  DCHECK(start_time_estimate != kNoTimestamp());
  if (stream->start_time == static_cast<int64_t>(AV_NOPTS_VALUE)) {
    return start_time_estimate == kInfiniteDuration() ? kNoTimestamp()
                                                      : start_time_estimate;
  }

  // First try the lower of the estimate and the |start_time| value.
  base::TimeDelta start_time =
      std::min(ConvertFromTimeBase(stream->time_base, stream->start_time),
               start_time_estimate);

  // Next see if the first buffered pts value is usable.
  if (stream->pts_buffer[0] != static_cast<int64_t>(AV_NOPTS_VALUE)) {
    const base::TimeDelta buffered_pts =
        ConvertFromTimeBase(stream->time_base, stream->pts_buffer[0]);
    if (buffered_pts < start_time)
      start_time = buffered_pts;
  }

  // NOTE: Do not use AVStream->first_dts since |start_time| should be a
  // presentation timestamp.
  return start_time;
}

// Some videos just want to watch the world burn, with a height of 0; cap the
// "infinite" aspect ratio resulting.
const int kInfiniteRatio = 99999;

// Common aspect ratios (multiplied by 100 and truncated) used for histogramming
// video sizes.  These were taken on 20111103 from
// http://wikipedia.org/wiki/Aspect_ratio_(image)#Previous_and_currently_used_aspect_ratios
const int kCommonAspectRatios100[] = {
    100, 115, 133, 137, 143, 150, 155, 160,  166,
    175, 177, 185, 200, 210, 220, 221, 235,  237,
    240, 255, 259, 266, 276, 293, 400, 1200, kInfiniteRatio,
};

template <class T>  // T has int width() & height() methods.
static void UmaHistogramAspectRatio(const char* name, const T& size) {
  UMA_HISTOGRAM_CUSTOM_ENUMERATION(
      name,
      // Intentionally use integer division to truncate the result.
      size.height() ? (size.width() * 100) / size.height() : kInfiniteRatio,
      base::CustomHistogram::ArrayToCustomRanges(
          kCommonAspectRatios100, arraysize(kCommonAspectRatios100)));
}

// Record detected track counts by type corresponding to a src= playback.
// Counts are split into 50 buckets, capped into [0,100] range.
static void RecordDetectedTrackTypeStats(int audio_count,
                                         int video_count,
                                         int text_count) {
  UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Audio", audio_count);
  UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Video", video_count);
  UMA_HISTOGRAM_COUNTS_100("Media.DetectedTrackCount.Text", text_count);
}

// Record audio decoder config UMA stats corresponding to a src= playback.
static void RecordAudioCodecStats(const AudioDecoderConfig& audio_config) {
  UMA_HISTOGRAM_ENUMERATION("Media.AudioCodec", audio_config.codec(),
                            kAudioCodecMax + 1);
  UMA_HISTOGRAM_ENUMERATION("Media.AudioSampleFormat",
                            audio_config.sample_format(), kSampleFormatMax + 1);
  UMA_HISTOGRAM_ENUMERATION("Media.AudioChannelLayout",
                            audio_config.channel_layout(),
                            CHANNEL_LAYOUT_MAX + 1);
  AudioSampleRate asr;
  if (ToAudioSampleRate(audio_config.samples_per_second(), &asr)) {
    UMA_HISTOGRAM_ENUMERATION("Media.AudioSamplesPerSecond", asr,
                              kAudioSampleRateMax + 1);
  } else {
    UMA_HISTOGRAM_COUNTS("Media.AudioSamplesPerSecondUnexpected",
                         audio_config.samples_per_second());
  }
}

// Record video decoder config UMA stats corresponding to a src= playback.
static void RecordVideoCodecStats(const VideoDecoderConfig& video_config,
                                  AVColorRange color_range) {
  UMA_HISTOGRAM_ENUMERATION("Media.VideoCodec", video_config.codec(),
                            kVideoCodecMax + 1);

  // Drop UNKNOWN because U_H_E() uses one bucket for all values less than 1.
  if (video_config.profile() >= 0) {
    UMA_HISTOGRAM_ENUMERATION("Media.VideoCodecProfile", video_config.profile(),
                              VIDEO_CODEC_PROFILE_MAX + 1);
  }
  UMA_HISTOGRAM_COUNTS_10000("Media.VideoCodedWidth",
                             video_config.coded_size().width());
  UmaHistogramAspectRatio("Media.VideoCodedAspectRatio",
                          video_config.coded_size());
  UMA_HISTOGRAM_COUNTS_10000("Media.VideoVisibleWidth",
                             video_config.visible_rect().width());
  UmaHistogramAspectRatio("Media.VideoVisibleAspectRatio",
                          video_config.visible_rect());
  UMA_HISTOGRAM_ENUMERATION("Media.VideoPixelFormatUnion",
                            video_config.format(), PIXEL_FORMAT_MAX + 1);
  UMA_HISTOGRAM_ENUMERATION("Media.VideoFrameColorSpace",
                            video_config.color_space(), COLOR_SPACE_MAX + 1);

  // Note the PRESUBMIT_IGNORE_UMA_MAX below, this silences the PRESUBMIT.py
  // check for uma enum max usage, since we're abusing
  // UMA_HISTOGRAM_ENUMERATION to report a discrete value.
  UMA_HISTOGRAM_ENUMERATION("Media.VideoColorRange", color_range,
                            AVCOL_RANGE_NB);  // PRESUBMIT_IGNORE_UMA_MAX
}

static const char kCodecNone[] = "none";

static const char* GetCodecName(const AVCodecContext* context) {
  if (context->codec_descriptor)
    return context->codec_descriptor->name;
  const AVCodecDescriptor* codec_descriptor =
      avcodec_descriptor_get(context->codec_id);
  // If the codec name can't be determined, return none for tracking.
  return codec_descriptor ? codec_descriptor->name : kCodecNone;
}

static void SetTimeProperty(MediaLogEvent* event,
                            const std::string& key,
                            base::TimeDelta value) {
  if (value == kInfiniteDuration())
    event->params.SetString(key, "kInfiniteDuration");
  else if (value == kNoTimestamp())
    event->params.SetString(key, "kNoTimestamp");
  else
    event->params.SetDouble(key, value.InSecondsF());
}

std::unique_ptr<FFmpegDemuxerStream> FFmpegDemuxerStream::Create(
    FFmpegDemuxer* demuxer,
    AVStream* stream,
    const scoped_refptr<MediaLog>& media_log) {
  if (!demuxer || !stream)
    return nullptr;

  std::unique_ptr<FFmpegDemuxerStream> demuxer_stream;
  std::unique_ptr<AudioDecoderConfig> audio_config;
  std::unique_ptr<VideoDecoderConfig> video_config;

  if (stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
    audio_config.reset(new AudioDecoderConfig());

    // IsValidConfig() checks that the codec is supported and that the channel
    // layout and sample format are valid.
    //
    // TODO(chcunningham): Change AVStreamToAudioDecoderConfig to check
    // IsValidConfig internally and return a null scoped_ptr if not valid.
    if (!AVStreamToAudioDecoderConfig(stream, audio_config.get()) ||
        !audio_config->IsValidConfig()) {
      MEDIA_LOG(ERROR, media_log)
          << "FFmpegDemuxer: failed creating audio stream";
      return nullptr;
    }

    MEDIA_LOG(INFO, media_log) << "FFmpegDemuxer: created audio stream, config "
                               << audio_config->AsHumanReadableString();
  } else if (stream->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
    video_config.reset(new VideoDecoderConfig());

    // IsValidConfig() checks that the codec is supported and that the channel
    // layout and sample format are valid.
    //
    // TODO(chcunningham): Change AVStreamToVideoDecoderConfig to check
    // IsValidConfig internally and return a null scoped_ptr if not valid.
    if (!AVStreamToVideoDecoderConfig(stream, video_config.get()) ||
        !video_config->IsValidConfig()) {
      MEDIA_LOG(ERROR, media_log)
          << "FFmpegDemuxer: failed creating video stream";
      return nullptr;
    }

    MEDIA_LOG(INFO, media_log) << "FFmpegDemuxer: created video stream, config "
                               << video_config->AsHumanReadableString();
  }

  return base::WrapUnique(new FFmpegDemuxerStream(
      demuxer, stream, std::move(audio_config), std::move(video_config)));
}

//
// FFmpegDemuxerStream
//
FFmpegDemuxerStream::FFmpegDemuxerStream(
    FFmpegDemuxer* demuxer,
    AVStream* stream,
    std::unique_ptr<AudioDecoderConfig> audio_config,
    std::unique_ptr<VideoDecoderConfig> video_config)
    : demuxer_(demuxer),
      task_runner_(base::ThreadTaskRunnerHandle::Get()),
      stream_(stream),
      audio_config_(audio_config.release()),
      video_config_(video_config.release()),
      type_(UNKNOWN),
      liveness_(LIVENESS_UNKNOWN),
      end_of_stream_(false),
      last_packet_timestamp_(kNoTimestamp()),
      last_packet_duration_(kNoTimestamp()),
      video_rotation_(VIDEO_ROTATION_0),
      fixup_negative_timestamps_(false) {
  DCHECK(demuxer_);

  bool is_encrypted = false;
  int rotation = 0;
  AVDictionaryEntry* rotation_entry = NULL;

  // Determine our media format.
  switch (stream->codec->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
      DCHECK(audio_config_.get() && !video_config_.get());
      type_ = AUDIO;
      is_encrypted = audio_config_->is_encrypted();
      break;
    case AVMEDIA_TYPE_VIDEO:
      DCHECK(video_config_.get() && !audio_config_.get());
      type_ = VIDEO;
      is_encrypted = video_config_->is_encrypted();

      rotation_entry = av_dict_get(stream->metadata, "rotate", NULL, 0);
      if (rotation_entry && rotation_entry->value && rotation_entry->value[0])
        base::StringToInt(rotation_entry->value, &rotation);

      switch (rotation) {
        case 0:
          break;
        case 90:
          video_rotation_ = VIDEO_ROTATION_90;
          break;
        case 180:
          video_rotation_ = VIDEO_ROTATION_180;
          break;
        case 270:
          video_rotation_ = VIDEO_ROTATION_270;
          break;
        default:
          LOG(ERROR) << "Unsupported video rotation metadata: " << rotation;
          break;
      }

      break;
    case AVMEDIA_TYPE_SUBTITLE:
      DCHECK(!video_config_.get() && !audio_config_.get());
      type_ = TEXT;
      break;
    default:
      NOTREACHED();
      break;
  }

  // Calculate the duration.
  duration_ = ConvertStreamTimestamp(stream->time_base, stream->duration);

  if (is_encrypted) {
    AVDictionaryEntry* key = av_dict_get(stream->metadata, "enc_key_id", NULL,
                                         0);
    DCHECK(key);
    DCHECK(key->value);
    if (!key || !key->value)
      return;
    base::StringPiece base64_key_id(key->value);
    std::string enc_key_id;
    base::Base64Decode(base64_key_id, &enc_key_id);
    DCHECK(!enc_key_id.empty());
    if (enc_key_id.empty())
      return;

    encryption_key_id_.assign(enc_key_id);
    demuxer_->OnEncryptedMediaInitData(EmeInitDataType::WEBM, enc_key_id);
  }
}

FFmpegDemuxerStream::~FFmpegDemuxerStream() {
  DCHECK(!demuxer_);
  DCHECK(read_cb_.is_null());
  DCHECK(buffer_queue_.IsEmpty());
}

void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) {
  DCHECK(task_runner_->BelongsToCurrentThread());

  if (!demuxer_ || end_of_stream_) {
    NOTREACHED() << "Attempted to enqueue packet on a stopped stream";
    return;
  }

#if defined(USE_PROPRIETARY_CODECS)
  // Convert the packet if there is a bitstream filter.
  if (packet->data && bitstream_converter_ &&
      !bitstream_converter_->ConvertPacket(packet.get())) {
    LOG(ERROR) << "Format conversion failed.";
  }
#endif

  // Get side data if any. For now, the only type of side_data is VP8 Alpha. We
  // keep this generic so that other side_data types in the future can be
  // handled the same way as well.
  av_packet_split_side_data(packet.get());

  scoped_refptr<DecoderBuffer> buffer;

  if (type() == DemuxerStream::TEXT) {
    int id_size = 0;
    uint8_t* id_data = av_packet_get_side_data(
        packet.get(), AV_PKT_DATA_WEBVTT_IDENTIFIER, &id_size);

    int settings_size = 0;
    uint8_t* settings_data = av_packet_get_side_data(
        packet.get(), AV_PKT_DATA_WEBVTT_SETTINGS, &settings_size);

    std::vector<uint8_t> side_data;
    MakeSideData(id_data, id_data + id_size,
                 settings_data, settings_data + settings_size,
                 &side_data);

    buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size,
                                     side_data.data(), side_data.size());
  } else {
    int side_data_size = 0;
    uint8_t* side_data = av_packet_get_side_data(
        packet.get(), AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, &side_data_size);

    std::unique_ptr<DecryptConfig> decrypt_config;
    int data_offset = 0;
    if ((type() == DemuxerStream::AUDIO && audio_config_->is_encrypted()) ||
        (type() == DemuxerStream::VIDEO && video_config_->is_encrypted())) {
      if (!WebMCreateDecryptConfig(
              packet->data, packet->size,
              reinterpret_cast<const uint8_t*>(encryption_key_id_.data()),
              encryption_key_id_.size(), &decrypt_config, &data_offset)) {
        LOG(ERROR) << "Creation of DecryptConfig failed.";
      }
    }

    // If a packet is returned by FFmpeg's av_parser_parse2() the packet will
    // reference inner memory of FFmpeg.  As such we should transfer the packet
    // into memory we control.
    if (side_data_size > 0) {
      buffer = DecoderBuffer::CopyFrom(packet.get()->data + data_offset,
                                       packet.get()->size - data_offset,
                                       side_data, side_data_size);
    } else {
      buffer = DecoderBuffer::CopyFrom(packet.get()->data + data_offset,
                                       packet.get()->size - data_offset);
    }

    int skip_samples_size = 0;
    const uint32_t* skip_samples_ptr =
        reinterpret_cast<const uint32_t*>(av_packet_get_side_data(
            packet.get(), AV_PKT_DATA_SKIP_SAMPLES, &skip_samples_size));
    const int kSkipSamplesValidSize = 10;
    const int kSkipEndSamplesOffset = 1;
    if (skip_samples_size >= kSkipSamplesValidSize) {
      // Because FFmpeg rolls codec delay and skip samples into one we can only
      // allow front discard padding on the first buffer.  Otherwise the discard
      // helper can't figure out which data to discard.  See AudioDiscardHelper.
      int discard_front_samples = base::ByteSwapToLE32(*skip_samples_ptr);
      if (last_packet_timestamp_ != kNoTimestamp() && discard_front_samples) {
        DLOG(ERROR) << "Skip samples are only allowed for the first packet.";
        discard_front_samples = 0;
      }

      const int discard_end_samples =
          base::ByteSwapToLE32(*(skip_samples_ptr + kSkipEndSamplesOffset));
      const int samples_per_second =
          audio_decoder_config().samples_per_second();
      buffer->set_discard_padding(std::make_pair(
          FramesToTimeDelta(discard_front_samples, samples_per_second),
          FramesToTimeDelta(discard_end_samples, samples_per_second)));
    }

    if (decrypt_config)
      buffer->set_decrypt_config(std::move(decrypt_config));
  }

  if (packet->duration >= 0) {
    buffer->set_duration(
        ConvertStreamTimestamp(stream_->time_base, packet->duration));
  } else {
    // TODO(wolenetz): Remove when FFmpeg stops returning negative durations.
    // https://crbug.com/394418
    DVLOG(1) << "FFmpeg returned a buffer with a negative duration! "
             << packet->duration;
    buffer->set_duration(kNoTimestamp());
  }

  // Note: If pts is AV_NOPTS_VALUE, stream_timestamp will be kNoTimestamp().
  const base::TimeDelta stream_timestamp =
      ConvertStreamTimestamp(stream_->time_base, packet->pts);

  if (stream_timestamp != kNoTimestamp()) {
    const bool is_audio = type() == AUDIO;

    // If this file has negative timestamps don't rebase any other stream types
    // against the negative starting time.
    base::TimeDelta start_time = demuxer_->start_time();
    if (fixup_negative_timestamps_ && !is_audio &&
        start_time < base::TimeDelta()) {
      start_time = base::TimeDelta();
    }

    // Don't rebase timestamps for positive start times, the HTML Media Spec
    // details this in section "4.8.10.6 Offsets into the media resource." We
    // will still need to rebase timestamps before seeking with FFmpeg though.
    if (start_time > base::TimeDelta())
      start_time = base::TimeDelta();

    buffer->set_timestamp(stream_timestamp - start_time);

    // If enabled, and no codec delay is present, mark audio packets with
    // negative timestamps for post-decode discard.
    if (fixup_negative_timestamps_ && is_audio &&
        stream_timestamp < base::TimeDelta() &&
        buffer->duration() != kNoTimestamp()) {
      if (!stream_->codec->delay) {
        DCHECK_EQ(buffer->discard_padding().first, base::TimeDelta());

        if (stream_timestamp + buffer->duration() < base::TimeDelta()) {
          DCHECK_EQ(buffer->discard_padding().second, base::TimeDelta());

          // Discard the entire packet if it's entirely before zero.
          buffer->set_discard_padding(
              std::make_pair(kInfiniteDuration(), base::TimeDelta()));
        } else {
          // Only discard part of the frame if it overlaps zero.
          buffer->set_discard_padding(std::make_pair(
              -stream_timestamp, buffer->discard_padding().second));
        }
      } else {
        // Verify that codec delay would cover discard and that we don't need to
        // mark the packet for post decode discard.  Since timestamps may be in
        // milliseconds and codec delay in nanosecond precision, round up to the
        // nearest millisecond.  See enable_negative_timestamp_fixups().
        DCHECK_LE(-std::ceil(FramesToTimeDelta(
                                 audio_decoder_config().codec_delay(),
                                 audio_decoder_config().samples_per_second())
                                 .InMillisecondsF()),
                  stream_timestamp.InMillisecondsF());
      }
    }
  } else {
    // If this happens on the first packet, decoders will throw an error.
    buffer->set_timestamp(kNoTimestamp());
  }

  if (last_packet_timestamp_ != kNoTimestamp()) {
    // FFmpeg doesn't support chained ogg correctly.  Instead of guaranteeing
    // continuity across links in the chain it uses the timestamp information
    // from each link directly.  Doing so can lead to timestamps which appear to
    // go backwards in time.
    //
    // If the new link starts with a negative timestamp or a timestamp less than
    // the original (positive) |start_time|, we will get a negative timestamp
    // here.  It's also possible FFmpeg returns kNoTimestamp() here if it's not
    // able to work out a timestamp using the previous link and the next.
    //
    // Fixing chained ogg is non-trivial, so for now just reuse the last good
    // timestamp.  The decoder will rewrite the timestamps to be sample accurate
    // later.  See http://crbug.com/396864.
    if (fixup_negative_timestamps_ &&
        (buffer->timestamp() == kNoTimestamp() ||
         buffer->timestamp() < last_packet_timestamp_)) {
      buffer->set_timestamp(last_packet_timestamp_ +
                            (last_packet_duration_ != kNoTimestamp()
                                 ? last_packet_duration_
                                 : base::TimeDelta::FromMicroseconds(1)));
    }

    // The demuxer should always output positive timestamps.
    DCHECK(buffer->timestamp() >= base::TimeDelta());
    DCHECK(buffer->timestamp() != kNoTimestamp());

    if (last_packet_timestamp_ < buffer->timestamp()) {
      buffered_ranges_.Add(last_packet_timestamp_, buffer->timestamp());
      demuxer_->NotifyBufferingChanged();
    }
  }

  if (packet.get()->flags & AV_PKT_FLAG_KEY)
    buffer->set_is_key_frame(true);

  last_packet_timestamp_ = buffer->timestamp();
  last_packet_duration_ = buffer->duration();

  buffer_queue_.Push(buffer);
  SatisfyPendingRead();
}

void FFmpegDemuxerStream::SetEndOfStream() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  end_of_stream_ = true;
  SatisfyPendingRead();
}

void FFmpegDemuxerStream::FlushBuffers() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(read_cb_.is_null()) << "There should be no pending read";

  // H264 and AAC require that we resend the header after flush.
  // Reset bitstream for converter to do so.
  // This is related to chromium issue 140371 (http://crbug.com/140371).
  ResetBitstreamConverter();

  buffer_queue_.Clear();
  end_of_stream_ = false;
  last_packet_timestamp_ = kNoTimestamp();
  last_packet_duration_ = kNoTimestamp();
}

void FFmpegDemuxerStream::Stop() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  buffer_queue_.Clear();
  if (!read_cb_.is_null()) {
    base::ResetAndReturn(&read_cb_).Run(
        DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer());
  }
  demuxer_ = NULL;
  stream_ = NULL;
  end_of_stream_ = true;
}

DemuxerStream::Type FFmpegDemuxerStream::type() const {
  DCHECK(task_runner_->BelongsToCurrentThread());
  return type_;
}

DemuxerStream::Liveness FFmpegDemuxerStream::liveness() const {
  DCHECK(task_runner_->BelongsToCurrentThread());
  return liveness_;
}

void FFmpegDemuxerStream::Read(const ReadCB& read_cb) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  CHECK(read_cb_.is_null()) << "Overlapping reads are not supported";
  read_cb_ = BindToCurrentLoop(read_cb);

  // Don't accept any additional reads if we've been told to stop.
  // The |demuxer_| may have been destroyed in the pipeline thread.
  //
  // TODO(scherkus): it would be cleaner to reply with an error message.
  if (!demuxer_) {
    base::ResetAndReturn(&read_cb_).Run(
        DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer());
    return;
  }

  SatisfyPendingRead();
}

void FFmpegDemuxerStream::EnableBitstreamConverter() {
  DCHECK(task_runner_->BelongsToCurrentThread());

#if defined(USE_PROPRIETARY_CODECS)
  InitBitstreamConverter();
#else
  NOTREACHED() << "Proprietary codecs not enabled.";
#endif
}

void FFmpegDemuxerStream::ResetBitstreamConverter() {
#if defined(USE_PROPRIETARY_CODECS)
  if (bitstream_converter_)
    InitBitstreamConverter();
#endif  // defined(USE_PROPRIETARY_CODECS)
}

void FFmpegDemuxerStream::InitBitstreamConverter() {
#if defined(USE_PROPRIETARY_CODECS)
  switch (stream_->codec->codec_id) {
    case AV_CODEC_ID_H264:
      bitstream_converter_.reset(
          new FFmpegH264ToAnnexBBitstreamConverter(stream_->codec));
      break;
#if BUILDFLAG(ENABLE_HEVC_DEMUXING)
    case AV_CODEC_ID_HEVC:
      bitstream_converter_.reset(
          new FFmpegH265ToAnnexBBitstreamConverter(stream_->codec));
      break;
#endif
    case AV_CODEC_ID_AAC:
      bitstream_converter_.reset(
          new FFmpegAACBitstreamConverter(stream_->codec));
      break;
    default:
      break;
  }
#endif  // defined(USE_PROPRIETARY_CODECS)
}

bool FFmpegDemuxerStream::SupportsConfigChanges() { return false; }

AudioDecoderConfig FFmpegDemuxerStream::audio_decoder_config() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_EQ(type_, AUDIO);
  DCHECK(audio_config_.get());
  return *audio_config_;
}

VideoDecoderConfig FFmpegDemuxerStream::video_decoder_config() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_EQ(type_, VIDEO);
  DCHECK(video_config_.get());
  return *video_config_;
}

VideoRotation FFmpegDemuxerStream::video_rotation() {
  return video_rotation_;
}

void FFmpegDemuxerStream::SetLiveness(Liveness liveness) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK_EQ(liveness_, LIVENESS_UNKNOWN);
  liveness_ = liveness;
}

base::TimeDelta FFmpegDemuxerStream::GetElapsedTime() const {
  return ConvertStreamTimestamp(stream_->time_base, stream_->cur_dts);
}

Ranges<base::TimeDelta> FFmpegDemuxerStream::GetBufferedRanges() const {
  return buffered_ranges_;
}

void FFmpegDemuxerStream::SatisfyPendingRead() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  if (!read_cb_.is_null()) {
    if (!buffer_queue_.IsEmpty()) {
      base::ResetAndReturn(&read_cb_).Run(
          DemuxerStream::kOk, buffer_queue_.Pop());
    } else if (end_of_stream_) {
      base::ResetAndReturn(&read_cb_).Run(
          DemuxerStream::kOk, DecoderBuffer::CreateEOSBuffer());
    }
  }

  // Have capacity? Ask for more!
  if (HasAvailableCapacity() && !end_of_stream_) {
    demuxer_->NotifyCapacityAvailable();
  }
}

bool FFmpegDemuxerStream::HasAvailableCapacity() {
  // TODO(scherkus): Remove this return and reenable time-based capacity
  // after our data sources support canceling/concurrent reads, see
  // http://crbug.com/165762 for details.
#if 1
  return !read_cb_.is_null();
#else
  // Try to have one second's worth of encoded data per stream.
  const base::TimeDelta kCapacity = base::TimeDelta::FromSeconds(1);
  return buffer_queue_.IsEmpty() || buffer_queue_.Duration() < kCapacity;
#endif
}

size_t FFmpegDemuxerStream::MemoryUsage() const {
  return buffer_queue_.data_size();
}

TextKind FFmpegDemuxerStream::GetTextKind() const {
  DCHECK_EQ(type_, DemuxerStream::TEXT);

  if (stream_->disposition & AV_DISPOSITION_CAPTIONS)
    return kTextCaptions;

  if (stream_->disposition & AV_DISPOSITION_DESCRIPTIONS)
    return kTextDescriptions;

  if (stream_->disposition & AV_DISPOSITION_METADATA)
    return kTextMetadata;

  return kTextSubtitles;
}

std::string FFmpegDemuxerStream::GetMetadata(const char* key) const {
  const AVDictionaryEntry* entry =
      av_dict_get(stream_->metadata, key, NULL, 0);
  return (entry == NULL || entry->value == NULL) ? "" : entry->value;
}

// static
base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp(
    const AVRational& time_base,
    int64_t timestamp) {
  if (timestamp == static_cast<int64_t>(AV_NOPTS_VALUE))
    return kNoTimestamp();

  return ConvertFromTimeBase(time_base, timestamp);
}

//
// FFmpegDemuxer
//
FFmpegDemuxer::FFmpegDemuxer(
    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
    DataSource* data_source,
    const EncryptedMediaInitDataCB& encrypted_media_init_data_cb,
    const MediaTracksUpdatedCB& media_tracks_updated_cb,
    const scoped_refptr<MediaLog>& media_log)
    : host_(NULL),
      task_runner_(task_runner),
      blocking_thread_("FFmpegDemuxer"),
      pending_read_(false),
      pending_seek_(false),
      data_source_(data_source),
      media_log_(media_log),
      bitrate_(0),
      start_time_(kNoTimestamp()),
      preferred_stream_for_seeking_(-1, kNoTimestamp()),
      fallback_stream_for_seeking_(-1, kNoTimestamp()),
      text_enabled_(false),
      duration_known_(false),
      encrypted_media_init_data_cb_(encrypted_media_init_data_cb),
      media_tracks_updated_cb_(media_tracks_updated_cb),
      weak_factory_(this) {
  DCHECK(task_runner_.get());
  DCHECK(data_source_);
  DCHECK(!media_tracks_updated_cb_.is_null());
}

FFmpegDemuxer::~FFmpegDemuxer() {
  // NOTE: This class is not destroyed on |task_runner|, so we must ensure that
  // there are no outstanding WeakPtrs by the time we reach here.
  DCHECK(!weak_factory_.HasWeakPtrs());
}

std::string FFmpegDemuxer::GetDisplayName() const {
  return "FFmpegDemuxer";
}

void FFmpegDemuxer::Initialize(DemuxerHost* host,
                               const PipelineStatusCB& status_cb,
                               bool enable_text_tracks) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  host_ = host;
  text_enabled_ = enable_text_tracks;

  url_protocol_.reset(new BlockingUrlProtocol(
      data_source_,
      BindToCurrentLoop(base::Bind(&FFmpegDemuxer::OnDataSourceError,
                                   base::Unretained(this)))));
  glue_.reset(new FFmpegGlue(url_protocol_.get()));
  AVFormatContext* format_context = glue_->format_context();

  // Disable ID3v1 tag reading to avoid costly seeks to end of file for data we
  // don't use.  FFmpeg will only read ID3v1 tags if no other metadata is
  // available, so add a metadata entry to ensure some is always present.
  av_dict_set(&format_context->metadata, "skip_id3v1_tags", "", 0);

  // Ensure ffmpeg doesn't give up too early while looking for stream params;
  // this does not increase the amount of data downloaded.  The default value
  // is 5 AV_TIME_BASE units (1 second each), which prevents some oddly muxed
  // streams from being detected properly; this value was chosen arbitrarily.
  format_context->max_analyze_duration = 60 * AV_TIME_BASE;

  // Open the AVFormatContext using our glue layer.
  CHECK(blocking_thread_.Start());
  base::PostTaskAndReplyWithResult(
      blocking_thread_.task_runner().get(), FROM_HERE,
      base::Bind(&FFmpegGlue::OpenContext, base::Unretained(glue_.get())),
      base::Bind(&FFmpegDemuxer::OnOpenContextDone, weak_factory_.GetWeakPtr(),
                 status_cb));
}

void FFmpegDemuxer::Stop() {
  DCHECK(task_runner_->BelongsToCurrentThread());

  // The order of Stop() and Abort() is important here.  If Abort() is called
  // first, control may pass into FFmpeg where it can destruct buffers that are
  // in the process of being fulfilled by the DataSource.
  data_source_->Stop();
  url_protocol_->Abort();

  // This will block until all tasks complete. Note that after this returns it's
  // possible for reply tasks (e.g., OnReadFrameDone()) to be queued on this
  // thread. Each of the reply task methods must check whether we've stopped the
  // thread and drop their results on the floor.
  blocking_thread_.Stop();

  StreamVector::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if (*iter)
      (*iter)->Stop();
  }

  data_source_ = NULL;

  // Invalidate WeakPtrs on |task_runner_|, destruction may happen on another
  // thread.
  weak_factory_.InvalidateWeakPtrs();
}

void FFmpegDemuxer::StartWaitingForSeek(base::TimeDelta seek_time) {}

void FFmpegDemuxer::CancelPendingSeek(base::TimeDelta seek_time) {}

void FFmpegDemuxer::Seek(base::TimeDelta time, const PipelineStatusCB& cb) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  CHECK(!pending_seek_);

  // TODO(scherkus): Inspect |pending_read_| and cancel IO via |blocking_url_|,
  // otherwise we can end up waiting for a pre-seek read to complete even though
  // we know we're going to drop it on the floor.

  // FFmpeg requires seeks to be adjusted according to the lowest starting time.
  // Since EnqueuePacket() rebased negative timestamps by the start time, we
  // must correct the shift here.
  //
  // Additionally, to workaround limitations in how we expose seekable ranges to
  // Blink (http://crbug.com/137275), we also want to clamp seeks before the
  // start time to the start time.
  base::TimeDelta seek_time = start_time_ < base::TimeDelta()
                                  ? time + start_time_
                                  : time < start_time_ ? start_time_ : time;

  // When seeking in an opus stream we need to ensure we deliver enough data to
  // satisfy the seek preroll; otherwise the audio at the actual seek time will
  // not be entirely accurate.
  FFmpegDemuxerStream* audio_stream = GetFFmpegStream(DemuxerStream::AUDIO);
  if (audio_stream) {
    const AudioDecoderConfig& config = audio_stream->audio_decoder_config();
    if (config.codec() == kCodecOpus)
      seek_time = std::max(start_time_, seek_time - config.seek_preroll());
  }

  // Choose the seeking stream based on whether it contains the seek time, if no
  // match can be found prefer the preferred stream.
  //
  // TODO(dalecurtis): Currently FFmpeg does not ensure that all streams in a
  // given container will demux all packets after the seek point.  Instead it
  // only guarantees that all packets after the file position of the seek will
  // be demuxed.  It's an open question whether FFmpeg should fix this:
  // http://lists.ffmpeg.org/pipermail/ffmpeg-devel/2014-June/159212.html
  // Tracked by http://crbug.com/387996.
  DCHECK(preferred_stream_for_seeking_.second != kNoTimestamp());
  const int stream_index =
      seek_time < preferred_stream_for_seeking_.second &&
              seek_time >= fallback_stream_for_seeking_.second
          ? fallback_stream_for_seeking_.first
          : preferred_stream_for_seeking_.first;
  DCHECK_NE(stream_index, -1);

  const AVStream* seeking_stream =
      glue_->format_context()->streams[stream_index];

  pending_seek_ = true;
  base::PostTaskAndReplyWithResult(
      blocking_thread_.task_runner().get(),
      FROM_HERE,
      base::Bind(&av_seek_frame,
                 glue_->format_context(),
                 seeking_stream->index,
                 ConvertToTimeBase(seeking_stream->time_base, seek_time),
                 // Always seek to a timestamp <= to the desired timestamp.
                 AVSEEK_FLAG_BACKWARD),
      base::Bind(
          &FFmpegDemuxer::OnSeekFrameDone, weak_factory_.GetWeakPtr(), cb));
}

base::Time FFmpegDemuxer::GetTimelineOffset() const {
  return timeline_offset_;
}

DemuxerStream* FFmpegDemuxer::GetStream(DemuxerStream::Type type) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  return GetFFmpegStream(type);
}

FFmpegDemuxerStream* FFmpegDemuxer::GetFFmpegStream(
    DemuxerStream::Type type) const {
  StreamVector::const_iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if (*iter && (*iter)->type() == type) {
      return *iter;
    }
  }
  return NULL;
}

base::TimeDelta FFmpegDemuxer::GetStartTime() const {
  return std::max(start_time_, base::TimeDelta());
}

void FFmpegDemuxer::AddTextStreams() {
  DCHECK(task_runner_->BelongsToCurrentThread());

  for (StreamVector::size_type idx = 0; idx < streams_.size(); ++idx) {
    FFmpegDemuxerStream* stream = streams_[idx];
    if (stream == NULL || stream->type() != DemuxerStream::TEXT)
      continue;

    TextKind kind = stream->GetTextKind();
    std::string title = stream->GetMetadata("title");
    std::string language = stream->GetMetadata("language");

    // TODO: Implement "id" metadata in FFMPEG.
    // See: http://crbug.com/323183
    host_->AddTextStream(stream, TextTrackConfig(kind, title, language,
        std::string()));
  }
}

int64_t FFmpegDemuxer::GetMemoryUsage() const {
  int64_t allocation_size = 0;
  for (const auto& stream : streams_) {
    if (stream)
      allocation_size += stream->MemoryUsage();
  }
  return allocation_size;
}

void FFmpegDemuxer::OnEncryptedMediaInitData(
    EmeInitDataType init_data_type,
    const std::string& encryption_key_id) {
  std::vector<uint8_t> key_id_local(encryption_key_id.begin(),
                                    encryption_key_id.end());
  encrypted_media_init_data_cb_.Run(init_data_type, key_id_local);
}

void FFmpegDemuxer::NotifyCapacityAvailable() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  ReadFrameIfNeeded();
}

void FFmpegDemuxer::NotifyBufferingChanged() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  Ranges<base::TimeDelta> buffered;
  FFmpegDemuxerStream* audio = GetFFmpegStream(DemuxerStream::AUDIO);
  FFmpegDemuxerStream* video = GetFFmpegStream(DemuxerStream::VIDEO);
  if (audio && video) {
    buffered =
        audio->GetBufferedRanges().IntersectionWith(video->GetBufferedRanges());
  } else if (audio) {
    buffered = audio->GetBufferedRanges();
  } else if (video) {
    buffered = video->GetBufferedRanges();
  }
  host_->OnBufferedTimeRangesChanged(buffered);
}

// Helper for calculating the bitrate of the media based on information stored
// in |format_context| or failing that the size and duration of the media.
//
// Returns 0 if a bitrate could not be determined.
static int CalculateBitrate(AVFormatContext* format_context,
                            const base::TimeDelta& duration,
                            int64_t filesize_in_bytes) {
  // If there is a bitrate set on the container, use it.
  if (format_context->bit_rate > 0)
    return format_context->bit_rate;

  // Then try to sum the bitrates individually per stream.
  int bitrate = 0;
  for (size_t i = 0; i < format_context->nb_streams; ++i) {
    AVCodecContext* codec_context = format_context->streams[i]->codec;
    bitrate += codec_context->bit_rate;
  }
  if (bitrate > 0)
    return bitrate;

  // See if we can approximate the bitrate as long as we have a filesize and
  // valid duration.
  if (duration.InMicroseconds() <= 0 ||
      duration == kInfiniteDuration() ||
      filesize_in_bytes == 0) {
    return 0;
  }

  // Do math in floating point as we'd overflow an int64_t if the filesize was
  // larger than ~1073GB.
  double bytes = filesize_in_bytes;
  double duration_us = duration.InMicroseconds();
  return bytes * 8000000.0 / duration_us;
}

void FFmpegDemuxer::OnOpenContextDone(const PipelineStatusCB& status_cb,
                                      bool result) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  if (!blocking_thread_.IsRunning()) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state";
    status_cb.Run(PIPELINE_ERROR_ABORT);
    return;
  }

  if (!result) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": open context failed";
    status_cb.Run(DEMUXER_ERROR_COULD_NOT_OPEN);
    return;
  }

  // Fully initialize AVFormatContext by parsing the stream a little.
  base::PostTaskAndReplyWithResult(
      blocking_thread_.task_runner().get(),
      FROM_HERE,
      base::Bind(&avformat_find_stream_info,
                 glue_->format_context(),
                 static_cast<AVDictionary**>(NULL)),
      base::Bind(&FFmpegDemuxer::OnFindStreamInfoDone,
                 weak_factory_.GetWeakPtr(),
                 status_cb));
}

void FFmpegDemuxer::OnFindStreamInfoDone(const PipelineStatusCB& status_cb,
                                         int result) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  if (!blocking_thread_.IsRunning() || !data_source_) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state";
    status_cb.Run(PIPELINE_ERROR_ABORT);
    return;
  }

  if (result < 0) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName()
                                 << ": find stream info failed";
    status_cb.Run(DEMUXER_ERROR_COULD_NOT_PARSE);
    return;
  }

  // Create demuxer stream entries for each possible AVStream. Each stream
  // is examined to determine if it is supported or not (is the codec enabled
  // for it in this release?). Unsupported streams are skipped, allowing for
  // partial playback. At least one audio or video stream must be playable.
  AVFormatContext* format_context = glue_->format_context();
  streams_.resize(format_context->nb_streams);

  // Estimate the start time for each stream by looking through the packets
  // buffered during avformat_find_stream_info().  These values will be
  // considered later when determining the actual stream start time.
  //
  // These packets haven't been completely processed yet, so only look through
  // these values if the AVFormatContext has a valid start time.
  //
  // If no estimate is found, the stream entry will be kInfiniteDuration().
  std::vector<base::TimeDelta> start_time_estimates(format_context->nb_streams,
                                                    kInfiniteDuration());
  const AVFormatInternal* internal = format_context->internal;
  if (internal && internal->packet_buffer &&
      format_context->start_time != static_cast<int64_t>(AV_NOPTS_VALUE)) {
    struct AVPacketList* packet_buffer = internal->packet_buffer;
    while (packet_buffer != internal->packet_buffer_end) {
      DCHECK_LT(static_cast<size_t>(packet_buffer->pkt.stream_index),
                start_time_estimates.size());
      const AVStream* stream =
          format_context->streams[packet_buffer->pkt.stream_index];
      if (packet_buffer->pkt.pts != static_cast<int64_t>(AV_NOPTS_VALUE)) {
        const base::TimeDelta packet_pts =
            ConvertFromTimeBase(stream->time_base, packet_buffer->pkt.pts);
        if (packet_pts < start_time_estimates[stream->index])
          start_time_estimates[stream->index] = packet_pts;
      }
      packet_buffer = packet_buffer->next;
    }
  }

  std::unique_ptr<MediaTracks> media_tracks(new MediaTracks());
  AVStream* audio_stream = NULL;
  AudioDecoderConfig audio_config;
  AVStream* video_stream = NULL;
  VideoDecoderConfig video_config;

  // If available, |start_time_| will be set to the lowest stream start time.
  start_time_ = kInfiniteDuration();

  base::TimeDelta max_duration;
  int detected_audio_track_count = 0;
  int detected_video_track_count = 0;
  int detected_text_track_count = 0;
  for (size_t i = 0; i < format_context->nb_streams; ++i) {
    AVStream* stream = format_context->streams[i];
    const AVCodecContext* codec_context = stream->codec;
    const AVMediaType codec_type = codec_context->codec_type;

    if (codec_type == AVMEDIA_TYPE_AUDIO) {
      // Log the codec detected, whether it is supported or not, and whether or
      // not we have already detected a supported codec in another stream.
      UMA_HISTOGRAM_SPARSE_SLOWLY("Media.DetectedAudioCodecHash",
                                  HashCodecName(GetCodecName(codec_context)));
      detected_audio_track_count++;

      if (audio_stream) {
        MEDIA_LOG(INFO, media_log_) << GetDisplayName()
                                    << ": skipping extra audio track";
        continue;
      }
    } else if (codec_type == AVMEDIA_TYPE_VIDEO) {
      // Log the codec detected, whether it is supported or not, and whether or
      // not we have already detected a supported codec in another stream.
      UMA_HISTOGRAM_SPARSE_SLOWLY("Media.DetectedVideoCodecHash",
                                  HashCodecName(GetCodecName(codec_context)));
      detected_video_track_count++;

      if (video_stream) {
        MEDIA_LOG(INFO, media_log_) << GetDisplayName()
                                    << ": skipping extra video track";
        continue;
      }

#if BUILDFLAG(ENABLE_HEVC_DEMUXING)
      if (stream->codec->codec_id == AV_CODEC_ID_HEVC) {
        // If ffmpeg is built without HEVC parser/decoder support, it will be
        // able to demux HEVC based solely on container-provided information,
        // but unable to get some of the parameters without parsing the stream
        // (e.g. coded size needs to be read from SPS, pixel format is typically
        // deduced from decoder config in hvcC box). These are not really needed
        // when using external decoder (e.g. hardware decoder), so override them
        // here, to make sure this translates into a valid VideoDecoderConfig.
        if (stream->codec->coded_width == 0 &&
            stream->codec->coded_height == 0) {
          DCHECK(stream->codec->width > 0);
          DCHECK(stream->codec->height > 0);
          stream->codec->coded_width = stream->codec->width;
          stream->codec->coded_height = stream->codec->height;
        }
        if (stream->codec->pix_fmt == AV_PIX_FMT_NONE) {
          stream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
        }
      }
#endif
    } else if (codec_type == AVMEDIA_TYPE_SUBTITLE) {
      detected_text_track_count++;
      if (codec_context->codec_id != AV_CODEC_ID_WEBVTT || !text_enabled_) {
        continue;
      }
    } else {
      continue;
    }

    // Attempt to create a FFmpegDemuxerStream from the AVStream. This will
    // return nullptr if the AVStream is invalid. Validity checks will verify
    // things like: codec, channel layout, sample/pixel format, etc...
    std::unique_ptr<FFmpegDemuxerStream> demuxer_stream =
        FFmpegDemuxerStream::Create(this, stream, media_log_);
    if (demuxer_stream.get()) {
      streams_[i] = demuxer_stream.release();
    } else {
      if (codec_type == AVMEDIA_TYPE_AUDIO) {
        MEDIA_LOG(INFO, media_log_)
            << GetDisplayName()
            << ": skipping invalid or unsupported audio track";
      } else if (codec_type == AVMEDIA_TYPE_VIDEO) {
        MEDIA_LOG(INFO, media_log_)
            << GetDisplayName()
            << ": skipping invalid or unsupported video track";
      }

      // This AVStream does not successfully convert.
      continue;
    }

    std::string track_id = base::IntToString(stream->id);
    std::string track_label = streams_[i]->GetMetadata("handler_name");
    std::string track_language = streams_[i]->GetMetadata("language");

    // Some metadata is named differently in FFmpeg for webm files.
    if (strstr(format_context->iformat->name, "webm") ||
        strstr(format_context->iformat->name, "matroska")) {
      // TODO(servolk): FFmpeg doesn't set stream->id correctly for webm files.
      // Need to fix that and use it as track id. crbug.com/323183
      track_id = base::UintToString(media_tracks->tracks().size() + 1);
      track_label = streams_[i]->GetMetadata("title");
    }

    // Note when we find our audio/video stream (we only want one of each) and
    // record src= playback UMA stats for the stream's decoder config.
    if (codec_type == AVMEDIA_TYPE_AUDIO) {
      CHECK(!audio_stream);
      audio_stream = stream;
      audio_config = streams_[i]->audio_decoder_config();
      RecordAudioCodecStats(audio_config);

      media_tracks->AddAudioTrack(audio_config, track_id, "main", track_label,
                                  track_language);
    } else if (codec_type == AVMEDIA_TYPE_VIDEO) {
      CHECK(!video_stream);
      video_stream = stream;
      video_config = streams_[i]->video_decoder_config();
      RecordVideoCodecStats(video_config, stream->codec->color_range);

      media_tracks->AddVideoTrack(video_config, track_id, "main", track_label,
                                  track_language);
    }

    max_duration = std::max(max_duration, streams_[i]->duration());

    const base::TimeDelta start_time =
        ExtractStartTime(stream, start_time_estimates[i]);
    const bool has_start_time = start_time != kNoTimestamp();

    // Always prefer the video stream for seeking.  If none exists, we'll swap
    // the fallback stream with the preferred stream below.
    if (codec_type == AVMEDIA_TYPE_VIDEO) {
      preferred_stream_for_seeking_ =
          StreamSeekInfo(i, has_start_time ? start_time : base::TimeDelta());
    }

    if (!has_start_time)
      continue;

    if (start_time < start_time_) {
      start_time_ = start_time;

      // Choose the stream with the lowest starting time as the fallback stream
      // for seeking.  Video should always be preferred.
      fallback_stream_for_seeking_ = StreamSeekInfo(i, start_time);
    }
  }

  RecordDetectedTrackTypeStats(detected_audio_track_count,
                               detected_video_track_count,
                               detected_text_track_count);

  if (!audio_stream && !video_stream) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName()
                                 << ": no supported streams";
    status_cb.Run(DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
    return;
  }

  if (text_enabled_)
    AddTextStreams();

  if (format_context->duration != static_cast<int64_t>(AV_NOPTS_VALUE)) {
    // If there is a duration value in the container use that to find the
    // maximum between it and the duration from A/V streams.
    const AVRational av_time_base = {1, AV_TIME_BASE};
    max_duration =
        std::max(max_duration,
                 ConvertFromTimeBase(av_time_base, format_context->duration));
  } else {
    // The duration is unknown, in which case this is likely a live stream.
    max_duration = kInfiniteDuration();
  }

  // FFmpeg represents audio data marked as before the beginning of stream as
  // having negative timestamps.  This data must be discarded after it has been
  // decoded, not before since it is used to warmup the decoder.  There are
  // currently two known cases for this: vorbis in ogg and opus in ogg and webm.
  //
  // For API clarity, it was decided that the rest of the media pipeline should
  // not be exposed to negative timestamps.  Which means we need to rebase these
  // negative timestamps and mark them for discard post decoding.
  //
  // Post-decode frame dropping for packets with negative timestamps is outlined
  // in section A.2 in the Ogg Vorbis spec:
  // http://xiph.org/vorbis/doc/Vorbis_I_spec.html
  //
  // FFmpeg's use of negative timestamps for opus pre-skip is nonstandard, but
  // for more information on pre-skip see section 4.2 of the Ogg Opus spec:
  // https://tools.ietf.org/html/draft-ietf-codec-oggopus-08#section-4.2
  if (audio_stream && (audio_stream->codec->codec_id == AV_CODEC_ID_OPUS ||
                       (strcmp(format_context->iformat->name, "ogg") == 0 &&
                        audio_stream->codec->codec_id == AV_CODEC_ID_VORBIS))) {
    for (size_t i = 0; i < streams_.size(); ++i) {
      if (streams_[i])
        streams_[i]->enable_negative_timestamp_fixups();
    }

    // Fixup the seeking information to avoid selecting the audio stream simply
    // because it has a lower starting time.
    if (fallback_stream_for_seeking_.first == audio_stream->index &&
        fallback_stream_for_seeking_.second < base::TimeDelta()) {
      fallback_stream_for_seeking_.second = base::TimeDelta();
    }
  }

  // If no start time could be determined, default to zero and prefer the video
  // stream over the audio stream for seeking.  E.g., The WAV demuxer does not
  // put timestamps on its frames.
  if (start_time_ == kInfiniteDuration()) {
    start_time_ = base::TimeDelta();
    preferred_stream_for_seeking_ = StreamSeekInfo(
        video_stream ? video_stream->index : audio_stream->index, start_time_);
  } else if (!video_stream) {
    // If no video stream exists, use the audio or text stream found above.
    preferred_stream_for_seeking_ = fallback_stream_for_seeking_;
  }

  // MPEG-4 B-frames cause grief for a simple container like AVI. Enable PTS
  // generation so we always get timestamps, see http://crbug.com/169570
  if (strcmp(format_context->iformat->name, "avi") == 0)
    format_context->flags |= AVFMT_FLAG_GENPTS;

  // For testing purposes, don't overwrite the timeline offset if set already.
  if (timeline_offset_.is_null())
    timeline_offset_ = ExtractTimelineOffset(format_context);

  // Since we're shifting the externally visible start time to zero, we need to
  // adjust the timeline offset to compensate.
  if (!timeline_offset_.is_null() && start_time_ < base::TimeDelta())
    timeline_offset_ += start_time_;

  if (max_duration == kInfiniteDuration() && !timeline_offset_.is_null()) {
    SetLiveness(DemuxerStream::LIVENESS_LIVE);
  } else if (max_duration != kInfiniteDuration()) {
    SetLiveness(DemuxerStream::LIVENESS_RECORDED);
  } else {
    SetLiveness(DemuxerStream::LIVENESS_UNKNOWN);
  }

  // Good to go: set the duration and bitrate and notify we're done
  // initializing.
  host_->SetDuration(max_duration);
  duration_known_ = (max_duration != kInfiniteDuration());

  int64_t filesize_in_bytes = 0;
  url_protocol_->GetSize(&filesize_in_bytes);
  bitrate_ = CalculateBitrate(format_context, max_duration, filesize_in_bytes);
  if (bitrate_ > 0)
    data_source_->SetBitrate(bitrate_);

  // Use a single MediaLogEvent to batch all parameter updates at once; this
  // prevents throttling of events due to the large number of updates here.
  std::unique_ptr<MediaLogEvent> metadata_event =
      media_log_->CreateEvent(MediaLogEvent::PROPERTY_CHANGE);

  // Audio logging.
  metadata_event->params.SetBoolean("found_audio_stream", !!audio_stream);
  if (audio_stream) {
    const AVCodecContext* audio_codec = audio_stream->codec;
    metadata_event->params.SetString("audio_codec_name",
                                     GetCodecName(audio_codec));
    metadata_event->params.SetInteger("audio_channels_count",
                                      audio_codec->channels);
    metadata_event->params.SetString(
        "audio_sample_format",
        SampleFormatToString(audio_config.sample_format()));
    metadata_event->params.SetInteger("audio_samples_per_second",
                                      audio_config.samples_per_second());
  }

  // Video logging
  metadata_event->params.SetBoolean("found_video_stream", !!video_stream);
  if (video_stream) {
    const AVCodecContext* video_codec = video_stream->codec;
    metadata_event->params.SetString("video_codec_name",
                                     GetCodecName(video_codec));
    metadata_event->params.SetInteger("width", video_codec->width);
    metadata_event->params.SetInteger("height", video_codec->height);
    metadata_event->params.SetInteger("coded_width", video_codec->coded_width);
    metadata_event->params.SetInteger("coded_height",
                                      video_codec->coded_height);
    metadata_event->params.SetString(
        "time_base", base::StringPrintf("%d/%d", video_codec->time_base.num,
                                        video_codec->time_base.den));
    metadata_event->params.SetString(
        "video_format", VideoPixelFormatToString(video_config.format()));
    metadata_event->params.SetBoolean("video_is_encrypted",
                                      video_config.is_encrypted());
  }

  SetTimeProperty(metadata_event.get(), "max_duration", max_duration);
  SetTimeProperty(metadata_event.get(), "start_time", start_time_);
  metadata_event->params.SetInteger("bitrate", bitrate_);
  media_log_->AddEvent(std::move(metadata_event));

  media_tracks_updated_cb_.Run(std::move(media_tracks));

  status_cb.Run(PIPELINE_OK);
}

void FFmpegDemuxer::OnSeekFrameDone(const PipelineStatusCB& cb, int result) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  CHECK(pending_seek_);
  pending_seek_ = false;

  if (!blocking_thread_.IsRunning()) {
    MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": bad state";
    cb.Run(PIPELINE_ERROR_ABORT);
    return;
  }

  if (result < 0) {
    // Use VLOG(1) instead of NOTIMPLEMENTED() to prevent the message being
    // captured from stdout and contaminates testing.
    // TODO(scherkus): Implement this properly and signal error (BUG=23447).
    VLOG(1) << "Not implemented";
  }

  // Tell streams to flush buffers due to seeking.
  StreamVector::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if (*iter)
      (*iter)->FlushBuffers();
  }

  // Resume reading until capacity.
  ReadFrameIfNeeded();

  // Notify we're finished seeking.
  cb.Run(PIPELINE_OK);
}

void FFmpegDemuxer::ReadFrameIfNeeded() {
  DCHECK(task_runner_->BelongsToCurrentThread());

  // Make sure we have work to do before reading.
  if (!blocking_thread_.IsRunning() || !StreamsHaveAvailableCapacity() ||
      pending_read_ || pending_seek_) {
    return;
  }

  // Allocate and read an AVPacket from the media. Save |packet_ptr| since
  // evaluation order of packet.get() and base::Passed(&packet) is
  // undefined.
  ScopedAVPacket packet(new AVPacket());
  AVPacket* packet_ptr = packet.get();

  pending_read_ = true;
  base::PostTaskAndReplyWithResult(
      blocking_thread_.task_runner().get(),
      FROM_HERE,
      base::Bind(&av_read_frame, glue_->format_context(), packet_ptr),
      base::Bind(&FFmpegDemuxer::OnReadFrameDone,
                 weak_factory_.GetWeakPtr(),
                 base::Passed(&packet)));
}

void FFmpegDemuxer::OnReadFrameDone(ScopedAVPacket packet, int result) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  DCHECK(pending_read_);
  pending_read_ = false;

  if (!blocking_thread_.IsRunning() || pending_seek_) {
    return;
  }

  // Consider the stream as ended if:
  // - either underlying ffmpeg returned an error
  // - or FFMpegDemuxer reached the maximum allowed memory usage.
  if (result < 0 || IsMaxMemoryUsageReached()) {
    // Update the duration based on the highest elapsed time across all streams
    // if it was previously unknown.
    if (!duration_known_) {
      base::TimeDelta max_duration;

      for (StreamVector::iterator iter = streams_.begin();
           iter != streams_.end();
           ++iter) {
        if (!*iter)
          continue;

        base::TimeDelta duration = (*iter)->GetElapsedTime();
        if (duration != kNoTimestamp() && duration > max_duration)
          max_duration = duration;
      }

      if (max_duration > base::TimeDelta()) {
        host_->SetDuration(max_duration);
        duration_known_ = true;
      }
    }
    // If we have reached the end of stream, tell the downstream filters about
    // the event.
    StreamHasEnded();
    return;
  }

  // Queue the packet with the appropriate stream.
  DCHECK_GE(packet->stream_index, 0);
  DCHECK_LT(packet->stream_index, static_cast<int>(streams_.size()));

  // Defend against ffmpeg giving us a bad stream index.
  if (packet->stream_index >= 0 &&
      packet->stream_index < static_cast<int>(streams_.size()) &&
      streams_[packet->stream_index]) {
    // TODO(scherkus): Fix demuxing upstream to never return packets w/o data
    // when av_read_frame() returns success code. See bug comment for ideas:
    //
    // https://code.google.com/p/chromium/issues/detail?id=169133#c10
    if (!packet->data) {
      ScopedAVPacket new_packet(new AVPacket());
      av_new_packet(new_packet.get(), 0);
      av_packet_copy_props(new_packet.get(), packet.get());
      packet.swap(new_packet);
    }

    FFmpegDemuxerStream* demuxer_stream = streams_[packet->stream_index];
    demuxer_stream->EnqueuePacket(std::move(packet));
  }

  // Keep reading until we've reached capacity.
  ReadFrameIfNeeded();
}

bool FFmpegDemuxer::StreamsHaveAvailableCapacity() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  StreamVector::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if (*iter && (*iter)->HasAvailableCapacity()) {
      return true;
    }
  }
  return false;
}

bool FFmpegDemuxer::IsMaxMemoryUsageReached() const {
  DCHECK(task_runner_->BelongsToCurrentThread());

  // Max allowed memory usage, all streams combined.
  const size_t kDemuxerMemoryLimit = 150 * 1024 * 1024;

  size_t memory_left = kDemuxerMemoryLimit;
  for (StreamVector::const_iterator iter = streams_.begin();
       iter != streams_.end(); ++iter) {
    if (!(*iter))
      continue;

    size_t stream_memory_usage = (*iter)->MemoryUsage();
    if (stream_memory_usage > memory_left)
      return true;
    memory_left -= stream_memory_usage;
  }
  return false;
}

void FFmpegDemuxer::StreamHasEnded() {
  DCHECK(task_runner_->BelongsToCurrentThread());
  StreamVector::iterator iter;
  for (iter = streams_.begin(); iter != streams_.end(); ++iter) {
    if (!*iter)
      continue;
    (*iter)->SetEndOfStream();
  }
}

void FFmpegDemuxer::OnDataSourceError() {
  MEDIA_LOG(ERROR, media_log_) << GetDisplayName() << ": data source error";
  host_->OnDemuxerError(PIPELINE_ERROR_READ);
}

void FFmpegDemuxer::SetLiveness(DemuxerStream::Liveness liveness) {
  DCHECK(task_runner_->BelongsToCurrentThread());
  for (const auto& stream : streams_) {  // |stream| is a ref to a pointer.
    if (stream)
      stream->SetLiveness(liveness);
  }
}

}  // namespace media