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.os.SystemClock; 20 21 import java.util.ArrayList; 22 import java.util.HashMap; 23 import java.util.Map; 24 import java.util.concurrent.atomic.AtomicBoolean; 25 26 /** 27 * Filters are the processing nodes of the filter graphs. 28 * 29 * Filters may have any number of input and output ports, through which the data frames flow. 30 * TODO: More documentation on filter life-cycle, port and type checking, GL and RenderScript, ... 31 */ 32 public abstract class Filter { 33 34 private static class State { 35 private static final int STATE_UNPREPARED = 1; 36 private static final int STATE_PREPARED = 2; 37 private static final int STATE_OPEN = 3; 38 private static final int STATE_CLOSED = 4; 39 private static final int STATE_DESTROYED = 5; 40 41 public int current = STATE_UNPREPARED; 42 43 public synchronized boolean check(int state) { 44 return current == state; 45 } 46 47 } 48 49 private final int REQUEST_FLAG_NONE = 0; 50 private final int REQUEST_FLAG_CLOSE = 1; 51 52 private String mName; 53 private MffContext mContext; 54 private FilterGraph mFilterGraph; 55 56 private State mState = new State(); 57 private int mRequests = REQUEST_FLAG_NONE; 58 59 private int mMinimumAvailableInputs = 1; 60 private int mMinimumAvailableOutputs = 1; 61 62 private int mScheduleCount = 0; 63 private long mLastScheduleTime = 0; 64 65 private boolean mIsActive = true; 66 private AtomicBoolean mIsSleeping = new AtomicBoolean(false); 67 68 private long mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET; 69 70 private HashMap<String, InputPort> mConnectedInputPorts = new HashMap<String, InputPort>(); 71 private HashMap<String, OutputPort> mConnectedOutputPorts = new HashMap<String, OutputPort>(); 72 73 private InputPort[] mConnectedInputPortArray = null; 74 private OutputPort[] mConnectedOutputPortArray = null; 75 76 private ArrayList<Frame> mAutoReleaseFrames = new ArrayList<Frame>(); 77 78 79 /** 80 * Constructs a new filter. 81 * A filter is bound to a specific MffContext. Its name can be any String value, but it must 82 * be unique within the filter graph. 83 * 84 * Note that names starting with "$" are reserved for internal use, and should not be used. 85 * 86 * @param context The MffContext in which the filter will live. 87 * @param name The name of the filter. 88 */ 89 protected Filter(MffContext context, String name) { 90 mName = name; 91 mContext = context; 92 } 93 94 /** 95 * Checks whether the filter class is available on this platform. 96 * Some filters may not be installed on all platforms and can therefore not be instantiated. 97 * Before instantiating a filter, check if it is available by using this method. 98 * 99 * This method uses the shared FilterFactory to check whether the filter class is available. 100 * 101 * @param filterClassName The fully qualified class name of the Filter class. 102 * @return true, if filters of the specified class name are available. 103 */ 104 public static final boolean isAvailable(String filterClassName) { 105 return FilterFactory.sharedFactory().isFilterAvailable(filterClassName); 106 } 107 108 /** 109 * Returns the name of this filter. 110 * 111 * @return the name of the filter (specified during construction). 112 */ 113 public String getName() { 114 return mName; 115 } 116 117 /** 118 * Returns the signature of this filter. 119 * 120 * Subclasses should override this and return their filter signature. The default 121 * implementation returns a generic signature with no constraints. 122 * 123 * This method may be called at any time. 124 * 125 * @return the Signature instance for this filter. 126 */ 127 public Signature getSignature() { 128 return new Signature(); 129 } 130 131 /** 132 * Returns the MffContext that the filter resides in. 133 * 134 * @return the MffContext of the filter. 135 */ 136 public MffContext getContext() { 137 return mContext; 138 } 139 140 /** 141 * Returns true, if the filter is active. 142 * TODO: thread safety? 143 * 144 * @return true, if the filter is active. 145 */ 146 public boolean isActive() { 147 return mIsActive; 148 } 149 150 /** 151 * Activates the current filter. 152 * Only active filters can be scheduled for execution. This method can only be called if the 153 * GraphRunner that is executing the filter is stopped or paused. 154 */ 155 public void activate() { 156 assertIsPaused(); 157 if (!mIsActive) { 158 mIsActive = true; 159 } 160 } 161 162 /** 163 * Deactivates the current filter. 164 * Only active filters can be scheduled for execution. This method can only be called if the 165 * GraphRunner that is executing the filter is stopped or paused. 166 */ 167 public void deactivate() { 168 // TODO: Support close-on-deactivate (must happen in processing thread). 169 assertIsPaused(); 170 if (mIsActive) { 171 mIsActive = false; 172 } 173 } 174 175 /** 176 * Returns the filter's set of input ports. 177 * Note that this contains only the *connected* input ports. To retrieve all 178 * input ports that this filter accepts, one has to go via the filter's Signature. 179 * 180 * @return An array containing all connected input ports. 181 */ 182 public final InputPort[] getConnectedInputPorts() { 183 return mConnectedInputPortArray; 184 } 185 186 /** 187 * Returns the filter's set of output ports. 188 * Note that this contains only the *connected* output ports. To retrieve all 189 * output ports that this filter provides, one has to go via the filter's Signature. 190 * 191 * @return An array containing all connected output ports. 192 */ 193 public final OutputPort[] getConnectedOutputPorts() { 194 return mConnectedOutputPortArray; 195 } 196 197 /** 198 * Returns the input port with the given name. 199 * Note that this can only access the *connected* input ports. To retrieve all 200 * input ports that this filter accepts, one has to go via the filter's Signature. 201 * 202 * @return the input port with the specified name, or null if no connected input port 203 * with this name exists. 204 */ 205 public final InputPort getConnectedInputPort(String name) { 206 return mConnectedInputPorts.get(name); 207 } 208 209 /** 210 * Returns the output port with the given name. 211 * Note that this can only access the *connected* output ports. To retrieve all 212 * output ports that this filter provides, one has to go via the filter's Signature. 213 * 214 * @return the output port with the specified name, or null if no connected output port 215 * with this name exists. 216 */ 217 public final OutputPort getConnectedOutputPort(String name) { 218 return mConnectedOutputPorts.get(name); 219 } 220 221 /** 222 * Called when an input port has been attached in the graph. 223 * Override this method, in case you want to be informed of any connected input ports, or make 224 * modifications to them. Note that you may not assume that any other ports have been attached 225 * already. If you have dependencies on other ports, override 226 * {@link #onInputPortOpen(InputPort)}. The default implementation does nothing. 227 * 228 * @param port The InputPort instance that was attached. 229 */ 230 protected void onInputPortAttached(InputPort port) { 231 } 232 233 /** 234 * Called when an output port has been attached in the graph. 235 * Override this method, in case you want to be informed of any connected output ports, or make 236 * modifications to them. Note that you may not assume that any other ports have been attached 237 * already. If you have dependencies on other ports, override 238 * {@link #onOutputPortOpen(OutputPort)}. The default implementation does nothing. 239 * 240 * @param port The OutputPort instance that was attached. 241 */ 242 protected void onOutputPortAttached(OutputPort port) { 243 } 244 245 /** 246 * Called when an input port is opened on this filter. 247 * Input ports are opened by the data produce, that is the filter that is connected to an 248 * input port. Override this if you need to make modifications to the port before processing 249 * begins. Note, that this is only called if the connected filter is scheduled. You may assume 250 * that all ports are attached when this is called. 251 * 252 * @param port The InputPort instance that was opened. 253 */ 254 protected void onInputPortOpen(InputPort port) { 255 } 256 257 /** 258 * Called when an output port is opened on this filter. 259 * Output ports are opened when the filter they are attached to is opened. Override this if you 260 * need to make modifications to the port before processing begins. Note, that this is only 261 * called if the filter is scheduled. You may assume that all ports are attached when this is 262 * called. 263 * 264 * @param port The OutputPort instance that was opened. 265 */ 266 protected void onOutputPortOpen(OutputPort port) { 267 } 268 269 /** 270 * Returns true, if the filter is currently open. 271 * @return true, if the filter is currently open. 272 */ 273 public final boolean isOpen() { 274 return mState.check(State.STATE_OPEN); 275 } 276 277 @Override 278 public String toString() { 279 return mName + " (" + getClass().getSimpleName() + ")"; 280 } 281 282 /** 283 * Called when filter is prepared. 284 * Subclasses can override this to prepare the filter for processing. This method gets called 285 * once only just before the filter is scheduled for processing the first time. 286 * 287 * @see #onTearDown() 288 */ 289 protected void onPrepare() { 290 } 291 292 /** 293 * Called when the filter is opened. 294 * Subclasses can override this to perform any kind of initialization just before processing 295 * starts. This method may be called any number of times, but is always balanced with an 296 * {@link #onClose()} call. 297 * 298 * @see #onClose() 299 */ 300 protected void onOpen() { 301 } 302 303 /** 304 * Called to perform processing on Frame data. 305 * This is the only method subclasses must override. It is called every time the filter is 306 * ready for processing. Typically this is when there is input data to process and available 307 * output ports, but may differ depending on the port configuration. 308 */ 309 protected abstract void onProcess(); 310 311 /** 312 * Called when the filter is closed. 313 * Subclasses can override this to perform any kind of post-processing steps. Processing will 314 * not resume until {@link #onOpen()} is called again. This method is only called if the filter 315 * is open. 316 * 317 * @see #onOpen() 318 */ 319 protected void onClose() { 320 } 321 322 /** 323 * Called when the filter is torn down. 324 * Subclasses can override this to perform clean-up tasks just before the filter is disposed of. 325 * It is called when the filter graph that the filter belongs to is disposed. 326 * 327 * @see #onPrepare() 328 */ 329 protected void onTearDown() { 330 } 331 332 /** 333 * Check if the input conditions are met in order to schedule this filter. 334 * 335 * This is used by {@link #canSchedule()} to determine if the input-port conditions given by 336 * the filter are met. Subclasses that override scheduling behavior can make use of this 337 * function. 338 * 339 * @return true, if the filter's input conditions are met. 340 */ 341 protected boolean inputConditionsMet() { 342 if (mConnectedInputPortArray.length > 0) { 343 int inputFrames = 0; 344 // [Non-iterator looping] 345 for (int i = 0; i < mConnectedInputPortArray.length; ++i) { 346 if (!mConnectedInputPortArray[i].conditionsMet()) { 347 return false; 348 } else if (mConnectedInputPortArray[i].hasFrame()) { 349 ++inputFrames; 350 } 351 } 352 if (inputFrames < mMinimumAvailableInputs) { 353 return false; 354 } 355 } 356 return true; 357 } 358 359 /** 360 * Check if the output conditions are met in order to schedule this filter. 361 * 362 * This is used by {@link #canSchedule()} to determine if the output-port conditions given by 363 * the filter are met. Subclasses that override scheduling behavior can make use of this 364 * function. 365 * 366 * @return true, if the filter's output conditions are met. 367 */ 368 protected boolean outputConditionsMet() { 369 if (mConnectedOutputPortArray.length > 0) { 370 int availableOutputs = 0; 371 for (int i = 0; i < mConnectedOutputPortArray.length; ++i) { 372 if (!mConnectedOutputPortArray[i].conditionsMet()) { 373 return false; 374 } else if (mConnectedOutputPortArray[i].isAvailable()) { 375 ++availableOutputs; 376 } 377 } 378 if (availableOutputs < mMinimumAvailableOutputs) { 379 return false; 380 } 381 } 382 return true; 383 } 384 385 /** 386 * Check if the Filter is in a state so that it can be scheduled. 387 * 388 * When overriding the filter's {@link #canSchedule()} method, you should never allow 389 * scheduling a filter that is not in a schedulable state. This will result in undefined 390 * behavior. 391 * 392 * @return true, if the filter is in a schedulable state. 393 */ 394 protected boolean inSchedulableState() { 395 return (mIsActive && !mState.check(State.STATE_CLOSED)); 396 } 397 398 /** 399 * Returns true if the filter can be currently scheduled. 400 * 401 * Filters may override this method if they depend on custom factors that determine whether 402 * they can be scheduled or not. The scheduler calls this method to determine whether or not 403 * a filter can be scheduled for execution. It does not guarantee that it will be executed. 404 * It is strongly recommended to call super's implementation to make sure your filter can be 405 * scheduled based on its state, input and output ports. 406 * 407 * @return true, if the filter can be scheduled. 408 */ 409 protected boolean canSchedule() { 410 return inSchedulableState() && inputConditionsMet() && outputConditionsMet(); 411 } 412 413 /** 414 * Returns the current FrameManager instance. 415 * @return the current FrameManager instance or null if there is no FrameManager set up yet. 416 */ 417 protected final FrameManager getFrameManager() { 418 return mFilterGraph.mRunner != null ? mFilterGraph.mRunner.getFrameManager() : null; 419 } 420 421 /** 422 * Returns whether the GraphRunner for this filter is running. 423 * 424 * Generally, this method should not be used for performing operations that need to be carried 425 * out before running begins. Use {@link #performPreparation(Runnable)} for this. 426 * 427 * @return true, if the GraphRunner for this filter is running. 428 */ 429 protected final boolean isRunning() { 430 return mFilterGraph != null && mFilterGraph.mRunner != null 431 && mFilterGraph.mRunner.isRunning(); 432 } 433 434 /** 435 * Performs operations before the filter is running. 436 * 437 * Use this method when your filter requires to perform operations while the graph is not 438 * running. The filter will not be scheduled for execution until your method has completed 439 * execution. 440 */ 441 protected final boolean performPreparation(Runnable runnable) { 442 synchronized (mState) { 443 if (mState.current == State.STATE_OPEN) { 444 return false; 445 } else { 446 runnable.run(); 447 return true; 448 } 449 } 450 } 451 452 /** 453 * Request that this filter be closed after the current processing step. 454 * 455 * Implementations may call this within their {@link #onProcess()} calls to indicate that the 456 * filter is done processing and wishes to be closed. After such a request the filter will be 457 * closed and no longer receive {@link #onProcess()} calls. 458 * 459 * @see #onClose() 460 * @see #onProcess() 461 */ 462 protected final void requestClose() { 463 mRequests |= REQUEST_FLAG_CLOSE; 464 } 465 466 /** 467 * Sets the minimum number of input frames required to process. 468 * A filter will not be scheduled unless at least a certain number of input frames are available 469 * on the input ports. This is only relevant if the filter has input ports and is not waiting on 470 * all ports. 471 * The default value is 1. 472 * 473 * @param count the minimum number of frames required to process. 474 * @see #getMinimumAvailableInputs() 475 * @see #setMinimumAvailableOutputs(int) 476 * @see InputPort#setWaitsForFrame(boolean) 477 */ 478 protected final void setMinimumAvailableInputs(int count) { 479 mMinimumAvailableInputs = count; 480 } 481 482 /** 483 * Returns the minimum number of input frames required to process this filter. 484 * The default value is 1. 485 * 486 * @return the minimum number of input frames required to process. 487 * @see #setMinimumAvailableInputs(int) 488 */ 489 protected final int getMinimumAvailableInputs() { 490 return mMinimumAvailableInputs; 491 } 492 493 /** 494 * Sets the minimum number of available output ports required to process. 495 * A filter will not be scheduled unless atleast a certain number of output ports are available. 496 * This is only relevant if the filter has output ports and is not waiting on all ports. The 497 * default value is 1. 498 * 499 * @param count the minimum number of frames required to process. 500 * @see #getMinimumAvailableOutputs() 501 * @see #setMinimumAvailableInputs(int) 502 * @see OutputPort#setWaitsUntilAvailable(boolean) 503 */ 504 protected final void setMinimumAvailableOutputs(int count) { 505 mMinimumAvailableOutputs = count; 506 } 507 508 /** 509 * Returns the minimum number of available outputs required to process this filter. 510 * The default value is 1. 511 * 512 * @return the minimum number of available outputs required to process. 513 * @see #setMinimumAvailableOutputs(int) 514 */ 515 protected final int getMinimumAvailableOutputs() { 516 return mMinimumAvailableOutputs; 517 } 518 519 /** 520 * Puts the filter to sleep so that it is no longer scheduled. 521 * To resume scheduling the filter another thread must call wakeUp() on this filter. 522 */ 523 protected final void enterSleepState() { 524 mIsSleeping.set(true); 525 } 526 527 /** 528 * Wakes the filter and resumes scheduling. 529 * This is generally called from another thread to signal that this filter should resume 530 * processing. Does nothing if filter is not sleeping. 531 */ 532 protected final void wakeUp() { 533 if (mIsSleeping.getAndSet(false)) { 534 if (isRunning()) { 535 mFilterGraph.mRunner.signalWakeUp(); 536 } 537 } 538 } 539 540 /** 541 * Returns whether this Filter is allowed to use OpenGL. 542 * 543 * Filters may use OpenGL if the MffContext supports OpenGL and its GraphRunner allows it. 544 * 545 * @return true, if this Filter is allowed to use OpenGL. 546 */ 547 protected final boolean isOpenGLSupported() { 548 return mFilterGraph.mRunner.isOpenGLSupported(); 549 } 550 551 /** 552 * Connect an output port to an input port of another filter. 553 * Connects the output port with the specified name to the input port with the specified name 554 * of the specified filter. If the input or output ports do not exist already, they are 555 * automatically created and added to the respective filter. 556 */ 557 final void connect(String outputName, Filter targetFilter, String inputName) { 558 // Make sure not connected already 559 if (getConnectedOutputPort(outputName) != null) { 560 throw new RuntimeException("Attempting to connect already connected output port '" 561 + outputName + "' of filter " + this + "'!"); 562 } else if (targetFilter.getConnectedInputPort(inputName) != null) { 563 throw new RuntimeException("Attempting to connect already connected input port '" 564 + inputName + "' of filter " + targetFilter + "'!"); 565 } 566 567 // Establish connection 568 InputPort inputPort = targetFilter.newInputPort(inputName); 569 OutputPort outputPort = newOutputPort(outputName); 570 outputPort.setTarget(inputPort); 571 572 // Fire attachment callbacks 573 targetFilter.onInputPortAttached(inputPort); 574 onOutputPortAttached(outputPort); 575 576 // Update array of ports (which is maintained for more efficient access) 577 updatePortArrays(); 578 } 579 580 final Map<String, InputPort> getConnectedInputPortMap() { 581 return mConnectedInputPorts; 582 } 583 584 final Map<String, OutputPort> getConnectedOutputPortMap() { 585 return mConnectedOutputPorts; 586 } 587 588 final void execute() { 589 synchronized (mState) { 590 autoPullInputs(); 591 mLastScheduleTime = SystemClock.elapsedRealtime(); 592 if (mState.current == State.STATE_UNPREPARED) { 593 onPrepare(); 594 mState.current = State.STATE_PREPARED; 595 } 596 if (mState.current == State.STATE_PREPARED) { 597 openPorts(); 598 onOpen(); 599 mState.current = State.STATE_OPEN; 600 } 601 if (mState.current == State.STATE_OPEN) { 602 onProcess(); 603 if (mRequests != REQUEST_FLAG_NONE) { 604 processRequests(); 605 } 606 } 607 } 608 autoReleaseFrames(); 609 ++mScheduleCount; 610 } 611 612 final void performClose() { 613 synchronized (mState) { 614 if (mState.current == State.STATE_OPEN) { 615 onClose(); 616 mIsSleeping.set(false); 617 mState.current = State.STATE_CLOSED; 618 mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET; 619 } 620 } 621 } 622 623 final void softReset() { 624 synchronized (mState) { 625 performClose(); 626 if (mState.current == State.STATE_CLOSED) { 627 mState.current = State.STATE_PREPARED; 628 } 629 } 630 } 631 632 final void performTearDown() { 633 synchronized (mState) { 634 if (mState.current == State.STATE_OPEN) { 635 throw new RuntimeException("Attempting to tear-down filter " + this + " which is " 636 + "in an open state!"); 637 } else if (mState.current != State.STATE_DESTROYED 638 && mState.current != State.STATE_UNPREPARED) { 639 onTearDown(); 640 mState.current = State.STATE_DESTROYED; 641 } 642 } 643 } 644 645 final void insertIntoFilterGraph(FilterGraph graph) { 646 mFilterGraph = graph; 647 updatePortArrays(); 648 } 649 650 final int getScheduleCount() { 651 return mScheduleCount; 652 } 653 654 final void resetScheduleCount() { 655 mScheduleCount = 0; 656 } 657 658 final void openPorts() { 659 // Opening the output ports will open the connected input ports 660 for (OutputPort outputPort : mConnectedOutputPorts.values()) { 661 openOutputPort(outputPort); 662 } 663 } 664 665 final void addAutoReleaseFrame(Frame frame) { 666 mAutoReleaseFrames.add(frame); 667 } 668 669 final long getCurrentTimestamp() { 670 return mCurrentTimestamp; 671 } 672 673 final void onPulledFrameWithTimestamp(long timestamp) { 674 if (timestamp > mCurrentTimestamp || mCurrentTimestamp == Frame.TIMESTAMP_NOT_SET) { 675 mCurrentTimestamp = timestamp; 676 } 677 } 678 679 final void openOutputPort(OutputPort outPort) { 680 if (outPort.getQueue() == null) { 681 try { 682 FrameQueue.Builder builder = new FrameQueue.Builder(); 683 InputPort inPort = outPort.getTarget(); 684 outPort.onOpen(builder); 685 inPort.onOpen(builder); 686 Filter targetFilter = inPort.getFilter(); 687 String queueName = mName + "[" + outPort.getName() + "] -> " + targetFilter.mName 688 + "[" + inPort.getName() + "]"; 689 FrameQueue queue = builder.build(queueName); 690 outPort.setQueue(queue); 691 inPort.setQueue(queue); 692 } catch (RuntimeException e) { 693 throw new RuntimeException("Could not open output port " + outPort + "!", e); 694 } 695 } 696 } 697 698 final boolean isSleeping() { 699 return mIsSleeping.get(); 700 } 701 702 final long getLastScheduleTime() { 703 return mLastScheduleTime ; 704 } 705 706 private final void autoPullInputs() { 707 // [Non-iterator looping] 708 for (int i = 0; i < mConnectedInputPortArray.length; ++i) { 709 InputPort port = mConnectedInputPortArray[i]; 710 if (port.hasFrame() && port.isAutoPullEnabled()) { 711 mConnectedInputPortArray[i].pullFrame(); 712 } 713 } 714 } 715 716 private final void autoReleaseFrames() { 717 // [Non-iterator looping] 718 for (int i = 0; i < mAutoReleaseFrames.size(); ++i) { 719 mAutoReleaseFrames.get(i).release(); 720 } 721 mAutoReleaseFrames.clear(); 722 } 723 724 private final InputPort newInputPort(String name) { 725 InputPort result = mConnectedInputPorts.get(name); 726 if (result == null) { 727 Signature.PortInfo info = getSignature().getInputPortInfo(name); 728 result = new InputPort(this, name, info); 729 mConnectedInputPorts.put(name, result); 730 } 731 return result; 732 } 733 734 private final OutputPort newOutputPort(String name) { 735 OutputPort result = mConnectedOutputPorts.get(name); 736 if (result == null) { 737 Signature.PortInfo info = getSignature().getOutputPortInfo(name); 738 result = new OutputPort(this, name, info); 739 mConnectedOutputPorts.put(name, result); 740 } 741 return result; 742 } 743 744 private final void processRequests() { 745 if ((mRequests & REQUEST_FLAG_CLOSE) != 0) { 746 performClose(); 747 mRequests = REQUEST_FLAG_NONE; 748 } 749 } 750 751 private void assertIsPaused() { 752 GraphRunner runner = GraphRunner.current(); 753 if (runner != null && !runner.isPaused() && !runner.isStopped()) { 754 throw new RuntimeException("Attempting to modify filter state while runner is " 755 + "executing. Please pause or stop the runner first!"); 756 } 757 } 758 759 private final void updatePortArrays() { 760 // Copy our port-maps to arrays for faster non-iterator access 761 mConnectedInputPortArray = mConnectedInputPorts.values().toArray(new InputPort[0]); 762 mConnectedOutputPortArray = mConnectedOutputPorts.values().toArray(new OutputPort[0]); 763 } 764 765 } 766 767