1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package androidx.media.filterfw;
17
18import androidx.media.filterfw.GraphRunner.Listener;
19import androidx.media.filterfw.Signature.PortInfo;
20
21import com.google.common.util.concurrent.SettableFuture;
22
23import junit.framework.TestCase;
24
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.Map;
28import java.util.Map.Entry;
29import java.util.Set;
30import java.util.concurrent.ExecutionException;
31import java.util.concurrent.TimeUnit;
32import java.util.concurrent.TimeoutException;
33
34/**
35 * A {@link TestCase} for testing single MFF filter runs. Implementers should extend this class and
36 * implement the {@link #createFilter(MffContext)} method to create the filter under test. Inside
37 * each test method, the implementer should supply one or more frames for all the filter inputs
38 * (calling {@link #injectInputFrame(String, Frame)}) and then invoke {@link #process()}. Once the
39 * processing finishes, one should call {@link #getOutputFrame(String)} to get and inspect the
40 * output frames.
41 *
42 * TODO: extend this to deal with filters that push multiple output frames.
43 * TODO: relax the requirement that all output ports should be pushed (the implementer should be
44 *       able to tell which ports to wait for before process() returns).
45 * TODO: handle undeclared inputs and outputs.
46 */
47public abstract class MffFilterTestCase extends MffTestCase {
48
49    private static final long DEFAULT_TIMEOUT_MS = 1000;
50
51    private FilterGraph mGraph;
52    private GraphRunner mRunner;
53    private Map<String, Frame> mOutputFrames;
54    private Set<String> mEmptyOutputPorts;
55
56    private SettableFuture<Void> mProcessResult;
57
58    protected abstract Filter createFilter(MffContext mffContext);
59
60    @Override
61    protected void setUp() throws Exception {
62        super.setUp();
63        MffContext mffContext = getMffContext();
64        FilterGraph.Builder graphBuilder = new FilterGraph.Builder(mffContext);
65        Filter filterUnderTest = createFilter(mffContext);
66        graphBuilder.addFilter(filterUnderTest);
67
68        connectInputPorts(mffContext, graphBuilder, filterUnderTest);
69        connectOutputPorts(mffContext, graphBuilder, filterUnderTest);
70
71        mGraph = graphBuilder.build();
72        mRunner = mGraph.getRunner();
73        mRunner.setListener(new Listener() {
74            @Override
75            public void onGraphRunnerStopped(GraphRunner runner) {
76                mProcessResult.set(null);
77            }
78
79            @Override
80            public void onGraphRunnerError(Exception exception, boolean closedSuccessfully) {
81                mProcessResult.setException(exception);
82            }
83        });
84
85        mOutputFrames = new HashMap<String, Frame>();
86        mProcessResult = SettableFuture.create();
87    }
88
89    @Override
90    protected void tearDown() throws Exception {
91        for (Frame frame : mOutputFrames.values()) {
92            frame.release();
93        }
94        mOutputFrames = null;
95
96        mRunner.stop();
97        mRunner = null;
98        mGraph = null;
99
100        mProcessResult = null;
101        super.tearDown();
102    }
103
104    protected void injectInputFrame(String portName, Frame frame) {
105        FrameSourceFilter filter = (FrameSourceFilter) mGraph.getFilter("in_" + portName);
106        filter.injectFrame(frame);
107    }
108
109    /**
110     * Returns the frame pushed out by the filter under test. Should only be called after
111     * {@link #process(long)} has returned.
112     */
113    protected Frame getOutputFrame(String outputPortName) {
114        return mOutputFrames.get("out_" + outputPortName);
115    }
116
117    protected void process(long timeoutMs)
118            throws ExecutionException, TimeoutException, InterruptedException {
119        mRunner.start(mGraph);
120        mProcessResult.get(timeoutMs, TimeUnit.MILLISECONDS);
121    }
122
123    protected void process() throws ExecutionException, TimeoutException, InterruptedException {
124        process(DEFAULT_TIMEOUT_MS);
125    }
126
127    /**
128     * This method should be called to create the input frames inside the test cases (instead of
129     * {@link Frame#create(FrameType, int[])}). This is required to work around a requirement for
130     * the latter method to be called on the MFF thread.
131     */
132    protected Frame createFrame(FrameType type, int[] dimensions) {
133        return new Frame(type, dimensions, mRunner.getFrameManager());
134    }
135
136    private void connectInputPorts(
137            MffContext mffContext, FilterGraph.Builder graphBuilder, Filter filter) {
138        Signature signature = filter.getSignature();
139        for (Entry<String, PortInfo> inputPortEntry : signature.getInputPorts().entrySet()) {
140            Filter inputFilter = new FrameSourceFilter(mffContext, "in_" + inputPortEntry.getKey());
141            graphBuilder.addFilter(inputFilter);
142            graphBuilder.connect(inputFilter, "output", filter, inputPortEntry.getKey());
143        }
144    }
145
146    private void connectOutputPorts(
147            MffContext mffContext, FilterGraph.Builder graphBuilder, Filter filter) {
148        Signature signature = filter.getSignature();
149        mEmptyOutputPorts = new HashSet<String>();
150        OutputFrameListener outputFrameListener = new OutputFrameListener();
151        for (Entry<String, PortInfo> outputPortEntry : signature.getOutputPorts().entrySet()) {
152            FrameTargetFilter outputFilter = new FrameTargetFilter(
153                    mffContext, "out_" + outputPortEntry.getKey());
154            graphBuilder.addFilter(outputFilter);
155            graphBuilder.connect(filter, outputPortEntry.getKey(), outputFilter, "input");
156            outputFilter.setListener(outputFrameListener);
157            mEmptyOutputPorts.add("out_" + outputPortEntry.getKey());
158        }
159    }
160
161    private class OutputFrameListener implements FrameTargetFilter.Listener {
162
163        @Override
164        public void onFramePushed(String filterName, Frame frame) {
165            mOutputFrames.put(filterName, frame);
166            boolean alreadyPushed = !mEmptyOutputPorts.remove(filterName);
167            if (alreadyPushed) {
168                throw new IllegalStateException(
169                        "A frame has been pushed twice to the same output port.");
170            }
171            if (mEmptyOutputPorts.isEmpty()) {
172                // All outputs have been pushed, stop the graph.
173                mRunner.stop();
174            }
175        }
176
177    }
178
179}
180