1 /* 2 * Copyright (C) 2011 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 */ 16 17 package androidx.media.filterfw; 18 19 import android.util.Log; 20 import android.view.View; 21 import androidx.media.filterpacks.base.BranchFilter; 22 import androidx.media.filterpacks.base.FrameSlotSource; 23 import androidx.media.filterpacks.base.FrameSlotTarget; 24 import androidx.media.filterpacks.base.GraphInputSource; 25 import androidx.media.filterpacks.base.GraphOutputTarget; 26 import androidx.media.filterpacks.base.ValueTarget; 27 import androidx.media.filterpacks.base.ValueTarget.ValueListener; 28 import androidx.media.filterpacks.base.VariableSource; 29 30 import java.util.Collection; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.Map.Entry; 34 import java.util.Set; 35 36 /** 37 * A graph of Filter nodes. 38 * 39 * A FilterGraph instance contains a set of Filter instances connected by their output and input 40 * ports. Every filter belongs to exactly one graph and cannot be moved to another graph. 41 * 42 * FilterGraphs may contain sub-graphs that are dependent on the parent graph. These are typically 43 * used when inserting sub-graphs into MetaFilters. When a parent graph is torn down so are its 44 * sub-graphs. The same applies to flushing frames of a graph. 45 */ 46 public class FilterGraph { 47 48 private final static boolean DEBUG = false; 49 50 /** The context that this graph lives in */ 51 private MffContext mContext; 52 53 /** Map from name of filter to the filter instance */ 54 private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>(); 55 56 /** Allows quick access to array of all filters. */ 57 private Filter[] mAllFilters = null; 58 59 /** The GraphRunner currently attached to this graph */ 60 GraphRunner mRunner; 61 62 /** The set of sub-graphs of this graph */ 63 HashSet<FilterGraph> mSubGraphs = new HashSet<FilterGraph>(); 64 65 /** The parent graph of this graph, or null it this graph is a root graph. */ 66 private FilterGraph mParentGraph; 67 68 public static class Builder { 69 70 /** The context that this builder lives in */ 71 private MffContext mContext; 72 73 /** Map from name of filter to the filter instance */ 74 private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>(); 75 76 /** 77 * Creates a new builder for specifying a graph structure. 78 * @param context The context the graph will live in. 79 */ 80 public Builder(MffContext context) { 81 mContext = context; 82 } 83 84 /** 85 * Add a filter to the graph. 86 * 87 * Adds the specified filter to the set of filters of this graph. The filter must not be in 88 * the graph already, and the filter's name must be unique within the graph. 89 * 90 * @param filter the filter to add to the graph. 91 * @throws IllegalArgumentException if the filter is in the graph already, or its name is 92 * is already taken. 93 */ 94 public void addFilter(Filter filter) { 95 if (mFilterMap.values().contains(filter)) { 96 throw new IllegalArgumentException("Attempting to add filter " + filter + " that " 97 + "is in the graph already!"); 98 } else if (mFilterMap.containsKey(filter.getName())) { 99 throw new IllegalArgumentException("Graph contains filter with name '" 100 + filter.getName() + "' already!"); 101 } else { 102 mFilterMap.put(filter.getName(), filter); 103 } 104 } 105 106 /** 107 * Adds a variable to the graph. 108 * 109 * TODO: More documentation. 110 * 111 * @param name the name of the variable. 112 * @param value the value of the variable or null if no value is to be set yet. 113 * @return the VariableSource filter that holds the value of this variable. 114 */ 115 public VariableSource addVariable(String name, Object value) { 116 if (getFilter(name) != null) { 117 throw new IllegalArgumentException("Filter named '" + name + "' exists already!"); 118 } 119 VariableSource valueSource = new VariableSource(mContext, name); 120 addFilter(valueSource); 121 if (value != null) { 122 valueSource.setValue(value); 123 } 124 return valueSource; 125 } 126 127 public FrameSlotSource addFrameSlotSource(String name, String slotName) { 128 FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName); 129 addFilter(filter); 130 return filter; 131 } 132 133 public FrameSlotTarget addFrameSlotTarget(String name, String slotName) { 134 FrameSlotTarget filter = new FrameSlotTarget(mContext, name, slotName); 135 addFilter(filter); 136 return filter; 137 } 138 139 /** 140 * Connect two filters by their ports. 141 * The filters specified must have been previously added to the graph builder. 142 * 143 * @param sourceFilterName The name of the source filter. 144 * @param sourcePort The name of the source port. 145 * @param targetFilterName The name of the target filter. 146 * @param targetPort The name of the target port. 147 */ 148 public void connect(String sourceFilterName, String sourcePort, 149 String targetFilterName, String targetPort) { 150 Filter sourceFilter = getFilter(sourceFilterName); 151 Filter targetFilter = getFilter(targetFilterName); 152 if (sourceFilter == null) { 153 throw new IllegalArgumentException("Unknown filter '" + sourceFilterName + "'!"); 154 } else if (targetFilter == null) { 155 throw new IllegalArgumentException("Unknown filter '" + targetFilterName + "'!"); 156 } 157 connect(sourceFilter, sourcePort, targetFilter, targetPort); 158 } 159 160 /** 161 * Connect two filters by their ports. 162 * The filters specified must have been previously added to the graph builder. 163 * 164 * @param sourceFilter The source filter. 165 * @param sourcePort The name of the source port. 166 * @param targetFilter The target filter. 167 * @param targetPort The name of the target port. 168 */ 169 public void connect(Filter sourceFilter, String sourcePort, 170 Filter targetFilter, String targetPort) { 171 sourceFilter.connect(sourcePort, targetFilter, targetPort); 172 } 173 174 /** 175 * Returns the filter with the specified name. 176 * 177 * @return the filter with the specified name, or null if no such filter exists. 178 */ 179 public Filter getFilter(String name) { 180 return mFilterMap.get(name); 181 } 182 183 /** 184 * Builds the graph and checks signatures. 185 * 186 * @return The new graph instance. 187 */ 188 public FilterGraph build() { 189 checkSignatures(); 190 return buildWithParent(null); 191 } 192 193 /** 194 * Builds the sub-graph and checks signatures. 195 * 196 * @param parentGraph the parent graph of the built sub-graph. 197 * @return The new graph instance. 198 */ 199 public FilterGraph buildSubGraph(FilterGraph parentGraph) { 200 if (parentGraph == null) { 201 throw new NullPointerException("Parent graph must be non-null!"); 202 } 203 checkSignatures(); 204 return buildWithParent(parentGraph); 205 } 206 207 VariableSource assignValueToFilterInput(Object value, String filterName, String inputName) { 208 // Get filter to connect to 209 Filter filter = getFilter(filterName); 210 if (filter == null) { 211 throw new IllegalArgumentException("Unknown filter '" + filterName + "'!"); 212 } 213 214 // Construct a name for our value source and make sure it does not exist already 215 String valueSourceName = filterName + "." + inputName; 216 if (getFilter(valueSourceName) != null) { 217 throw new IllegalArgumentException("VariableSource for '" + filterName + "' and " 218 + "input '" + inputName + "' exists already!"); 219 } 220 221 // Create new VariableSource and connect it to the target filter and port 222 VariableSource valueSource = new VariableSource(mContext, valueSourceName); 223 addFilter(valueSource); 224 try { 225 ((Filter)valueSource).connect("value", filter, inputName); 226 } catch (RuntimeException e) { 227 throw new RuntimeException("Could not connect VariableSource to input '" + inputName 228 + "' of filter '" + filterName + "'!", e); 229 } 230 231 // Assign the value to the VariableSource 232 if (value != null) { 233 valueSource.setValue(value); 234 } 235 236 return valueSource; 237 } 238 239 VariableSource assignVariableToFilterInput(String varName, 240 String filterName, 241 String inputName) { 242 // Get filter to connect to 243 Filter filter = getFilter(filterName); 244 if (filter == null) { 245 throw new IllegalArgumentException("Unknown filter '" + filterName + "'!"); 246 } 247 248 // Get variable 249 Filter variable = getFilter(varName); 250 if (variable == null || !(variable instanceof VariableSource)) { 251 throw new IllegalArgumentException("Unknown variable '" + varName + "'!"); 252 } 253 254 // Connect variable (and possibly branch) variable to filter 255 try { 256 connectAndBranch(variable, "value", filter, inputName); 257 } catch (RuntimeException e) { 258 throw new RuntimeException("Could not connect VariableSource to input '" + inputName 259 + "' of filter '" + filterName + "'!", e); 260 } 261 262 return (VariableSource)variable; 263 } 264 265 /** 266 * Builds the graph without checking signatures. 267 * If parent is non-null, build a sub-graph of the specified parent. 268 * 269 * @return The new graph instance. 270 */ 271 private FilterGraph buildWithParent(FilterGraph parent) { 272 FilterGraph graph = new FilterGraph(mContext, parent); 273 graph.mFilterMap = mFilterMap; 274 graph.mAllFilters = mFilterMap.values().toArray(new Filter[0]); 275 for (Entry<String, Filter> filterEntry : mFilterMap.entrySet()) { 276 filterEntry.getValue().insertIntoFilterGraph(graph); 277 } 278 return graph; 279 } 280 281 private void checkSignatures() { 282 checkSignaturesForFilters(mFilterMap.values()); 283 } 284 285 // TODO: Currently this always branches even if the connection is a 1:1 connection. Later 286 // we may optimize to pass through directly in the 1:1 case (may require disconnecting 287 // ports). 288 private void connectAndBranch(Filter sourceFilter, 289 String sourcePort, 290 Filter targetFilter, 291 String targetPort) { 292 String branchName = "__" + sourceFilter.getName() + "_" + sourcePort + "Branch"; 293 Filter branch = getFilter(branchName); 294 if (branch == null) { 295 branch = new BranchFilter(mContext, branchName, false); 296 addFilter(branch); 297 sourceFilter.connect(sourcePort, branch, "input"); 298 } 299 String portName = "to" + targetFilter.getName() + "_" + targetPort; 300 branch.connect(portName, targetFilter, targetPort); 301 } 302 303 } 304 305 /** 306 * Attach the graph and its subgraphs to a custom GraphRunner. 307 * 308 * Call this if you want the graph to be executed by a specific GraphRunner. You must call 309 * this before any other runner is set. Note that calls to {@code getRunner()} and 310 * {@code run()} auto-create a GraphRunner. 311 * 312 * @param runner The GraphRunner instance that should execute this graph. 313 * @see #getRunner() 314 * @see #run() 315 */ 316 public void attachToRunner(GraphRunner runner) { 317 if (mRunner == null) { 318 for (FilterGraph subGraph : mSubGraphs) { 319 subGraph.attachToRunner(runner); 320 } 321 runner.attachGraph(this); 322 mRunner = runner; 323 } else if (mRunner != runner) { 324 throw new RuntimeException("Cannot attach FilterGraph to GraphRunner that is already " 325 + "attached to another GraphRunner!"); 326 } 327 } 328 329 /** 330 * Forcibly tear down a filter graph. 331 * 332 * Call this to release any resources associated with the filter graph, its filters and any of 333 * its sub-graphs. This method must not be called if the graph (or any sub-graph) is running. 334 * 335 * You may no longer access this graph instance or any of its subgraphs after calling this 336 * method. 337 * 338 * Tearing down of sub-graphs is not supported. You must tear down the root graph, which will 339 * tear down all of its sub-graphs. 340 * 341 * @throws IllegalStateException if the graph is still running. 342 * @throws RuntimeException if you attempt to tear down a sub-graph. 343 */ 344 public void tearDown() { 345 assertNotRunning(); 346 if (mParentGraph != null) { 347 throw new RuntimeException("Attempting to tear down sub-graph!"); 348 } 349 if (mRunner != null) { 350 mRunner.tearDownGraph(this); 351 } 352 for (FilterGraph subGraph : mSubGraphs) { 353 subGraph.mParentGraph = null; 354 subGraph.tearDown(); 355 } 356 mSubGraphs.clear(); 357 } 358 359 /** 360 * Returns the context of the graph. 361 * 362 * @return the MffContext instance that this graph is bound to. 363 */ 364 public MffContext getContext() { 365 return mContext; 366 } 367 368 /** 369 * Returns the filter with the specified name. 370 * 371 * @return the filter with the specified name, or null if no such filter exists. 372 */ 373 public Filter getFilter(String name) { 374 return mFilterMap.get(name); 375 } 376 377 /** 378 * Returns the VariableSource for the specified variable. 379 * 380 * TODO: More documentation. 381 * TODO: More specialized error handling. 382 * 383 * @param name The name of the VariableSource. 384 * @return The VariableSource filter instance with the specified name. 385 */ 386 public VariableSource getVariable(String name) { 387 Filter result = mFilterMap.get(name); 388 if (result != null && result instanceof VariableSource) { 389 return (VariableSource)result; 390 } else { 391 throw new IllegalArgumentException("Unknown variable '" + name + "' specified!"); 392 } 393 } 394 395 /** 396 * Returns the GraphOutputTarget with the specified name. 397 * 398 * @param name The name of the target. 399 * @return The GraphOutputTarget instance with the specified name. 400 */ 401 public GraphOutputTarget getGraphOutput(String name) { 402 Filter result = mFilterMap.get(name); 403 if (result != null && result instanceof GraphOutputTarget) { 404 return (GraphOutputTarget)result; 405 } else { 406 throw new IllegalArgumentException("Unknown target '" + name + "' specified!"); 407 } 408 } 409 410 /** 411 * Returns the GraphInputSource with the specified name. 412 * 413 * @param name The name of the source. 414 * @return The GraphInputSource instance with the specified name. 415 */ 416 public GraphInputSource getGraphInput(String name) { 417 Filter result = mFilterMap.get(name); 418 if (result != null && result instanceof GraphInputSource) { 419 return (GraphInputSource)result; 420 } else { 421 throw new IllegalArgumentException("Unknown source '" + name + "' specified!"); 422 } 423 } 424 425 /** 426 * Binds a filter to a view. 427 * 428 * ViewFilter instances support visualizing their data to a view. See the specific filter 429 * documentation for details. Views may be bound only if the graph is not running. 430 * 431 * @param filterName the name of the filter to bind. 432 * @param view the view to bind to. 433 * @throws IllegalStateException if the filter is in an illegal state. 434 * @throws IllegalArgumentException if no such view-filter exists. 435 */ 436 public void bindFilterToView(String filterName, View view) { 437 Filter filter = mFilterMap.get(filterName); 438 if (filter != null && filter instanceof ViewFilter) { 439 ((ViewFilter)filter).bindToView(view); 440 } else { 441 throw new IllegalArgumentException("Unknown view filter '" + filterName + "'!"); 442 } 443 } 444 445 /** 446 * TODO: Documentation. 447 */ 448 public void bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread) { 449 Filter filter = mFilterMap.get(filterName); 450 if (filter != null && filter instanceof ValueTarget) { 451 ((ValueTarget)filter).setListener(listener, onCallerThread); 452 } else { 453 throw new IllegalArgumentException("Unknown ValueTarget filter '" + filterName + "'!"); 454 } 455 } 456 457 // Running Graphs ////////////////////////////////////////////////////////////////////////////// 458 /** 459 * Convenience method to run the graph. 460 * 461 * Creates a new runner for this graph in the specified mode and executes it. Returns the 462 * runner to allow control of execution. 463 * 464 * @throws IllegalStateException if the graph is already running. 465 * @return the GraphRunner instance that was used for execution. 466 */ 467 public GraphRunner run() { 468 GraphRunner runner = getRunner(); 469 runner.setIsVerbose(false); 470 runner.start(this); 471 return runner; 472 } 473 474 /** 475 * Returns the GraphRunner for this graph. 476 * 477 * Every FilterGraph instance has a GraphRunner instance associated with it for executing the 478 * graph. 479 * 480 * @return the GraphRunner instance for this graph. 481 */ 482 public GraphRunner getRunner() { 483 if (mRunner == null) { 484 GraphRunner runner = new GraphRunner(mContext); 485 attachToRunner(runner); 486 } 487 return mRunner; 488 } 489 490 /** 491 * Returns whether the graph is currently running. 492 * 493 * @return true if the graph is currently running. 494 */ 495 public boolean isRunning() { 496 return mRunner != null && mRunner.isRunning(); 497 } 498 499 /** 500 * Check each filter's signatures if all requirements are fulfilled. 501 * 502 * This will throw a RuntimeException if any unfulfilled requirements are found. 503 * Note that FilterGraph.Builder also has a function checkSignatures(), which allows 504 * to do the same /before/ the FilterGraph is built. 505 */ 506 public void checkSignatures() { 507 checkSignaturesForFilters(mFilterMap.values()); 508 } 509 510 // MFF Internal Methods //////////////////////////////////////////////////////////////////////// 511 Filter[] getAllFilters() { 512 return mAllFilters; 513 } 514 515 static void checkSignaturesForFilters(Collection<Filter> filters) { 516 for (Filter filter : filters) { 517 if (DEBUG) { 518 Log.d("FilterGraph", "Checking filter " + filter.getName() + "..."); 519 } 520 Signature signature = filter.getSignature(); 521 signature.checkInputPortsConform(filter); 522 signature.checkOutputPortsConform(filter); 523 } 524 } 525 526 /** 527 * Wipes the filter references in this graph, so that they may be collected. 528 * 529 * This must be called only after a tearDown as this will make the FilterGraph invalid. 530 */ 531 void wipe() { 532 mAllFilters = null; 533 mFilterMap = null; 534 } 535 536 void flushFrames() { 537 for (Filter filter : mFilterMap.values()) { 538 for (InputPort inputPort : filter.getConnectedInputPorts()) { 539 inputPort.clear(); 540 } 541 for (OutputPort outputPort : filter.getConnectedOutputPorts()) { 542 outputPort.clear(); 543 } 544 } 545 } 546 547 Set<FilterGraph> getSubGraphs() { 548 return mSubGraphs; 549 } 550 551 // Internal Methods //////////////////////////////////////////////////////////////////////////// 552 private FilterGraph(MffContext context, FilterGraph parentGraph) { 553 mContext = context; 554 mContext.addGraph(this); 555 if (parentGraph != null) { 556 mParentGraph = parentGraph; 557 mParentGraph.mSubGraphs.add(this); 558 } 559 } 560 561 private void assertNotRunning() { 562 if (isRunning()) { 563 throw new IllegalStateException("Attempting to modify running graph!"); 564 } 565 } 566 } 567 568